39 Commits

Author SHA1 Message Date
c70f3eabfe Shamrock: typo BaseTransmitServlet.kt
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-02-19 11:30:22 +08:00
1c65aab673 refactor send_forward_msg(暂未完成 请勿使用) 2024-02-19 04:14:26 +08:00
a5cdd64686 upgrade workflow 2024-02-19 04:13:50 +08:00
b07ca5bd03 fix linq 2024-02-19 04:13:49 +08:00
8f8580d542 Shamrock: remove debug log #242
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-02-18 21:16:23 +08:00
0ed4480878 Shamrock: fix #242 2024-02-18 21:16:04 +08:00
c3e0031aa4 Shamrock: debug #242 2024-02-18 20:36:42 +08:00
388c963e88 Shamrock: fix markdown parser 2024-02-18 20:28:45 +08:00
4283651b1e Shamrock: fix comm_elem parser 2024-02-18 20:08:57 +08:00
50d7dfa06d Shamrock: support inline_keyboard msg 2024-02-18 20:00:37 +08:00
b3a2e605fb Shamrock: fix NT image acquisition 2024-02-18 18:52:56 +08:00
14bf5fc0a2 Shamrock: fix fakeReceiveSelfMsg 2024-02-18 07:51:43 +08:00
2c8b57a7dc Shamrock: fix #238 2024-02-18 07:45:48 +08:00
8e6c167987 Shamrock: fix #236 2024-02-18 07:37:47 +08:00
2c8094c8c8 Shamrock: 临时修补rkey缺失导致的qqnt图片无法获取 #236 2024-02-17 19:39:26 +08:00
62385d6f62 Shamrock: 修复合并转发获取图片错误 #236 2024-02-17 15:43:12 +08:00
3b210d7ed0 Shamrock: fix #236 2024-02-17 09:37:01 +08:00
63ce2d40bd Shamrock: fix get role by nt crash 2024-02-16 10:50:00 +08:00
36f8b6e54b Shamrock: Change the image upload source to a camera 2024-02-16 09:50:31 +08:00
58413044e9 Shamrock: typo log 2024-02-16 09:16:05 +08:00
3395cd9d95 Shamrock: 支持群临时消息推送携带群号以及群名称 2024-02-16 09:12:45 +08:00
494b1f1fd0 Shamrock: 允许禁止QQ启动无关紧要的进程服务 2024-02-16 00:09:35 +08:00
cf943fd13a Shamrock: atメッセージ優先nameパラメータ 2024-02-15 13:18:43 +08:00
9608b46799 Shamrock: 是正メッセージプッシュアイデンティティの取得が遅い 2024-02-15 13:14:37 +08:00
502956e3ec Shamrock: エイト・メッセージにニックネームの迅速なクエリを許可する 2024-02-15 13:01:34 +08:00
27b4c26da7 Shamrock: fix GlobalEventTransmitter x2 2024-02-11 14:28:07 +08:00
65f54360f8 Shamrock: not fix GlobalEventTransmitter x2 2024-02-11 14:06:10 +08:00
9a9fad975f Shamrock: not fix GlobalEventTransmitter 2024-02-11 13:57:58 +08:00
7153b21cd4 Shamrock: fix GlobalEventTransmitter 2024-02-11 13:42:28 +08:00
fdb2486090 Shamrock: Disable lost connection detection 2024-02-10 00:41:38 +08:00
d60b2a25d1 Update SECURITY.md 2024-02-09 08:04:40 +08:00
2d8dde6951 add history msg to database 2024-02-08 23:52:21 +08:00
78fd60dade Merge pull request #228 from Mythologyli/master
feat: get group applier uin from request msg
2024-02-08 22:34:40 +08:00
80dbf6af28 feat: get group applier uin from request msg 2024-02-08 22:27:55 +08:00
1e53753b5a Shamrock: fix #227 2024-02-08 20:17:51 +08:00
e727877268 Merge pull request #225 from MrXiaoM/fix-guild-message
修复 频道消息事件不符合 go-cqhttp 规范
2024-02-08 20:16:01 +08:00
63b69df3ea fix missing guild_id and channel_id 2024-02-08 14:51:14 +08:00
b03e02675b Shamrock: add timeout #223 2024-02-05 22:16:12 +08:00
e68a1ffd37 Shamrock: fix guild sync 2024-02-05 22:12:20 +08:00
80 changed files with 1966 additions and 527 deletions

View File

@ -22,13 +22,13 @@ jobs:
fetch-depth: 0 fetch-depth: 0
- name: Setup JDK 17 - name: Setup JDK 17
uses: actions/setup-java@v4.0.0 uses: actions/setup-java@v4
with: with:
java-version: 17 java-version: 17
distribution: "adopt" distribution: "adopt"
- name: Cache Gradle Dependencies - name: Cache Gradle Dependencies
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: | path: |
~/.gradle/caches ~/.gradle/caches
@ -38,7 +38,7 @@ jobs:
restore-keys: gradle-deps restore-keys: gradle-deps
- name: Cache Gradle Build - name: Cache Gradle Build
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: | path: |
~/.gradle/caches/build-cache-* ~/.gradle/caches/build-cache-*
@ -82,19 +82,19 @@ jobs:
echo "|x86_64|$x86_64" >> $GITHUB_STEP_SUMMARY echo "|x86_64|$x86_64" >> $GITHUB_STEP_SUMMARY
- name: Upload ALL APK RELEASE - name: Upload ALL APK RELEASE
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: "${{ env.SHAMROCK_VERSION_ALL }}" name: "${{ env.SHAMROCK_VERSION_ALL }}"
path: "${{ env.APK_FILE_ALL }}" path: "${{ env.APK_FILE_ALL }}"
- name: Upload ARM64 APK RELEASE - name: Upload ARM64 APK RELEASE
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: "${{ env.SHAMROCK_VERSION_ARM64 }}" name: "${{ env.SHAMROCK_VERSION_ARM64 }}"
path: "${{ env.APK_FILE_ARM64 }}" path: "${{ env.APK_FILE_ARM64 }}"
- name: Upload X86_64 APK RELEASE - name: Upload X86_64 APK RELEASE
uses: actions/upload-artifact@v3 uses: actions/upload-artifact@v4
with: with:
name: "${{ env.SHAMROCK_VERSION_x86_64 }}" name: "${{ env.SHAMROCK_VERSION_x86_64 }}"
path: "${{ env.APK_FILE_X86_64 }}" path: "${{ env.APK_FILE_X86_64 }}"

View File

@ -1,11 +1,19 @@
# Security Policy # Security Policy
## Support Version ## 支持的版本
| Version | Supported | | 版本 | 支持状态 |
| ------- | ------------------ | | ------- | ------------------ |
| 9.0.15 | :white_check_mark: |
| 8.9.75 | :white_check_mark: | | 8.9.75 | :white_check_mark: |
| 8.9.73 | :white_check_mark: | | 8.9.73 | :white_check_mark: |
| 8.9.98 | :white_check_mark: | | 8.9.98 | :white_check_mark: |
| < 8.9.68| :x: | | < 8.9.68| :x: |
## 频道支持性说明
如果需要使用`频道`相关功能请升级QQ到9.0.8版本
## Riru检测问题
QQ自`9.0.8`开始将会检测riru可能作为封号因素

View File

@ -229,6 +229,16 @@ object ShamrockConfig {
return preferences.getBoolean("anti_qq_trace", true) return preferences.getBoolean("anti_qq_trace", true)
} }
fun isForbidUselessProcess(ctx: Context): Boolean {
val preferences = ctx.getSharedPreferences("config", 0)
return preferences.getBoolean("forbid_useless_process", false)
}
fun setForbidUselessProcess(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("forbid_useless_process", v).apply()
}
fun setAntiTrace(ctx: Context, v: Boolean) { fun setAntiTrace(ctx: Context, v: Boolean) {
val preferences = ctx.getSharedPreferences("config", 0) val preferences = ctx.getSharedPreferences("config", 0)
preferences.edit().putBoolean("anti_qq_trace", v).apply() preferences.edit().putBoolean("anti_qq_trace", v).apply()
@ -333,6 +343,7 @@ object ShamrockConfig {
"alive_reply" to preferences.getBoolean("alive_reply", false), "alive_reply" to preferences.getBoolean("alive_reply", false),
"enable_sync_msg_as_sent_msg" to preferences.getBoolean("enable_sync_msg_as_sent_msg", false), "enable_sync_msg_as_sent_msg" to preferences.getBoolean("enable_sync_msg_as_sent_msg", false),
"disable_auto_sync_setting" to preferences.getBoolean("disable_auto_sync_setting", false), "disable_auto_sync_setting" to preferences.getBoolean("disable_auto_sync_setting", false),
"forbid_useless_process" to preferences.getBoolean("forbid_useless_process", false)
) )
} }

View File

@ -100,17 +100,16 @@ fun LabFragment() {
thickness = 0.2.dp thickness = 0.2.dp
) )
/*
Function( Function(
title = "自动清理QQ垃圾", title = "禁止无用进程",
desc = "也许会导致奇怪的问题(无效)。", desc = "禁止QQ生成无用进程浪费内存",
descColor = color, descColor = color,
isSwitch = ShamrockConfig.isAutoClean(ctx) isSwitch = ShamrockConfig.isForbidUselessProcess(ctx)
) { ) {
ShamrockConfig.setAutoClean(ctx, it) ShamrockConfig.setForbidUselessProcess(ctx, it)
ShamrockConfig.pushUpdate(ctx) ShamrockConfig.pushUpdate(ctx)
return@Function false return@Function true
}*/ }
Function( Function(
title = "自回复测试", title = "自回复测试",

View File

@ -5,53 +5,13 @@ import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber import kotlinx.serialization.protobuf.ProtoNumber
@Serializable
data class MessageBody(
@ProtoNumber(1) val msgHead: MessageHead? = null,
@ProtoNumber(2) val contentHead: MessageContentHead? = null,
@ProtoNumber(3) val richMsg: RichMessage? = null,
)
@Serializable @Serializable
data class RichMessage( data class RichMessage(
@ProtoNumber(1) val elements: MessageElementList? = null, @ProtoNumber(1) val font: Font? = null,
@ProtoNumber(2) val rawBuffer: ByteArray? = null,
)
@Serializable
data class MessageElementList(
@ProtoNumber(2) val elements: List<MessageElement>? = null @ProtoNumber(2) val elements: List<MessageElement>? = null
) )
@Serializable @Serializable
data class MessageElement( data class Font(
@ProtoNumber(51) val json: JsonElement? = null, @ProtoNumber(9) val fontName: String? = null
)
@Serializable
data class JsonElement(
@ProtoNumber(1) val data: ByteArray? = null,
)
@Serializable
data class MessageHead(
@ProtoNumber(1) val peer: Long = Long.MIN_VALUE,
@ProtoNumber(2) val peerUid: String? = null,
@ProtoNumber(3) val flag: Int = Int.MIN_VALUE,
@ProtoNumber(5) val receiver: Long? = null,
@ProtoNumber(6) val receiverUid: String? = null,
)
@Serializable
data class MessageContentHead(
@ProtoNumber(1) val msgType: Int = Int.MIN_VALUE,
@ProtoNumber(2) val msgSubType: Int = Int.MIN_VALUE,
@ProtoNumber(4) val u1: Long = Long.MIN_VALUE,
@ProtoNumber(5) val msgSeq: Long = Long.MIN_VALUE,
@ProtoNumber(6) val msgTime: Long? = null,
@ProtoNumber(7) val u2: Int? = null,
@ProtoNumber(11) val u3: Long? = null,
@ProtoNumber(12) val msgRandom: Long = Long.MIN_VALUE,
@ProtoNumber(14) val u4: Long? = null,
@ProtoNumber(28) val u5: Long? = null,
) )

View 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
)

View 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
)

View 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,
)

View 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,
)

View 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,
)

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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,
)

View File

@ -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,
)

View 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,
)
}
}

View File

@ -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
)

View File

@ -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,
)
}
}

View File

@ -2,11 +2,11 @@ package protobuf.push
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber import kotlinx.serialization.protobuf.ProtoNumber
import protobuf.message.MessageBody import protobuf.message.NtMessage
@Serializable @Serializable
data class MessagePush( data class MessagePush(
@ProtoNumber(1) val msgBody: MessageBody? = null, @ProtoNumber(1) val msgBody: NtMessage? = null,
@ProtoNumber(4) val clientInfo: MessagePushClientInfo? = null, @ProtoNumber(4) val clientInfo: MessagePushClientInfo? = null,
) )

View File

@ -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();
}

View File

@ -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();
}

View File

@ -0,0 +1,4 @@
package com.tencent.libra.request;
public class Option {
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -0,0 +1,7 @@
package com.tencent.mobileqq.qmmkv;
public class MMKVOptionEntity {
public String decodeString(String str, String str2) {
return "";
}
}

View File

@ -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;
}
}

View File

@ -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();
}

View File

@ -1,7 +1,9 @@
package com.tencent.qqnt.kernel.api.impl; package com.tencent.qqnt.kernel.api.impl;
import com.tencent.qqnt.kernel.nativeinterface.IGetTempChatInfoCallback;
import com.tencent.qqnt.kernel.nativeinterface.IKernelMsgListener; import com.tencent.qqnt.kernel.nativeinterface.IKernelMsgListener;
import com.tencent.qqnt.kernel.nativeinterface.IOperateCallback; import com.tencent.qqnt.kernel.nativeinterface.IOperateCallback;
import com.tencent.qqnt.kernel.nativeinterface.RichMediaElementGetReq;
import com.tencent.qqnt.kernel.nativeinterface.RichMediaFilePathInfo; import com.tencent.qqnt.kernel.nativeinterface.RichMediaFilePathInfo;
import com.tencent.qqnt.kernel.nativeinterface.TempChatPrepareInfo; import com.tencent.qqnt.kernel.nativeinterface.TempChatPrepareInfo;
@ -9,6 +11,10 @@ import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable; import org.jetbrains.annotations.Nullable;
public class MsgService { public class MsgService {
void getRichMediaElement(@NotNull RichMediaElementGetReq req) {
}
public void addMsgListener(IKernelMsgListener listener) { public void addMsgListener(IKernelMsgListener listener) {
} }
@ -24,4 +30,8 @@ public class MsgService {
public void prepareTempChat(TempChatPrepareInfo tempChatPrepareInfo, IOperateCallback cb) { public void prepareTempChat(TempChatPrepareInfo tempChatPrepareInfo, IOperateCallback cb) {
} }
public void getTempChatInfo(int chatType, @Nullable String uid, @Nullable IGetTempChatInfoCallback cb) {
}
} }

View File

@ -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;
}
}

View File

@ -0,0 +1,5 @@
package com.tencent.qqnt.kernel.nativeinterface;
public interface IGetTempChatInfoCallback {
void onResult(int code, String msg, TempChatInfo info);
}

View File

@ -79,6 +79,10 @@ public final class InlineKeyboardButton {
return "InlineKeyboardButton{id=" + this.id + ",label=" + this.label + ",visitedLabel=" + this.visitedLabel + ",style=" + this.style + ",type=" + this.type + ",clickLimit=" + this.clickLimit + ",unsupportTips=" + this.unsupportTips + ",data=" + this.data + ",atBotShowChannelList=" + this.atBotShowChannelList + ",permissionType=" + this.permissionType + ",specifyRoleIds=" + this.specifyRoleIds + ",specifyTinyids=" + this.specifyTinyids + ",}"; return "InlineKeyboardButton{id=" + this.id + ",label=" + this.label + ",visitedLabel=" + this.visitedLabel + ",style=" + this.style + ",type=" + this.type + ",clickLimit=" + this.clickLimit + ",unsupportTips=" + this.unsupportTips + ",data=" + this.data + ",atBotShowChannelList=" + this.atBotShowChannelList + ",permissionType=" + this.permissionType + ",specifyRoleIds=" + this.specifyRoleIds + ",specifyTinyids=" + this.specifyTinyids + ",}";
} }
public InlineKeyboardButton(String str, String str2, String str3, int i, int i2, int i3, String str4, String str5, boolean z, int i4, ArrayList<String> arrayList, ArrayList<String> arrayList2, boolean z2, int i5, boolean z3, ArrayList<SubscribeMsgTemplateID> arrayList3) {
}
public InlineKeyboardButton(String str, String str2, String str3, int i2, int i3, int i4, String str4, String str5, boolean z, int i5, ArrayList<String> arrayList, ArrayList<String> arrayList2) { public InlineKeyboardButton(String str, String str2, String str3, int i2, int i3, int i4, String str4, String str5, boolean z, int i5, ArrayList<String> arrayList, ArrayList<String> arrayList2) {
this.id = ""; this.id = "";
this.label = ""; this.label = "";

View File

@ -264,6 +264,10 @@ public final class PicElement implements IKernelModel {
this.transferStatus = num; this.transferStatus = num;
} }
public int getStoreID() {
return 0;
}
public String toString() { public String toString() {
return "PicElement{picSubType=" + this.picSubType + ",fileName=" + this.fileName + ",fileSize=" + this.fileSize + ",picWidth=" + this.picWidth + ",picHeight=" + this.picHeight + ",original=" + this.original + ",md5HexStr=" + this.md5HexStr + ",sourcePath=" + this.sourcePath + ",thumbPath=" + this.thumbPath + ",transferStatus=" + this.transferStatus + ",progress=" + this.progress + ",picType=" + this.picType + ",invalidState=" + this.invalidState + ",fileUuid=" + this.fileUuid + ",fileSubId=" + this.fileSubId + ",thumbFileSize=" + this.thumbFileSize + ",fileBizId=" + this.fileBizId + ",downloadIndex=" + this.downloadIndex + ",summary=" + this.summary + ",emojiFrom=" + this.emojiFrom + ",emojiWebUrl=" + this.emojiWebUrl + ",emojiAd=" + this.emojiAd + ",emojiMall=" + this.emojiMall + ",emojiZplan=" + this.emojiZplan + ",originImageMd5=" + this.originImageMd5 + ",originImageUrl=" + this.originImageUrl + ",importRichMediaContext=" + this.importRichMediaContext + ",isFlashPic=" + this.isFlashPic + ",}"; return "PicElement{picSubType=" + this.picSubType + ",fileName=" + this.fileName + ",fileSize=" + this.fileSize + ",picWidth=" + this.picWidth + ",picHeight=" + this.picHeight + ",original=" + this.original + ",md5HexStr=" + this.md5HexStr + ",sourcePath=" + this.sourcePath + ",thumbPath=" + this.thumbPath + ",transferStatus=" + this.transferStatus + ",progress=" + this.progress + ",picType=" + this.picType + ",invalidState=" + this.invalidState + ",fileUuid=" + this.fileUuid + ",fileSubId=" + this.fileSubId + ",thumbFileSize=" + this.thumbFileSize + ",fileBizId=" + this.fileBizId + ",downloadIndex=" + this.downloadIndex + ",summary=" + this.summary + ",emojiFrom=" + this.emojiFrom + ",emojiWebUrl=" + this.emojiWebUrl + ",emojiAd=" + this.emojiAd + ",emojiMall=" + this.emojiMall + ",emojiZplan=" + this.emojiZplan + ",originImageMd5=" + this.originImageMd5 + ",originImageUrl=" + this.originImageUrl + ",importRichMediaContext=" + this.importRichMediaContext + ",isFlashPic=" + this.isFlashPic + ",}";
} }

View File

@ -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;
}
}

View File

@ -0,0 +1,4 @@
package com.tencent.qqnt.kernel.nativeinterface;
public class SubscribeMsgTemplateID {
}

View File

@ -0,0 +1,4 @@
package epic;
public class EIPCClient {
}

View File

@ -0,0 +1,11 @@
package epic;
import android.os.Bundle;
public class EIPCResult {
public Bundle data;
public boolean isSuccess() {
return false;
}
}

View File

@ -59,6 +59,7 @@ import moe.fuqiuluo.shamrock.remote.service.data.EssenceMessage
import moe.fuqiuluo.shamrock.remote.service.data.GroupAnnouncement import moe.fuqiuluo.shamrock.remote.service.data.GroupAnnouncement
import moe.fuqiuluo.shamrock.remote.service.data.GroupAnnouncementMessage import moe.fuqiuluo.shamrock.remote.service.data.GroupAnnouncementMessage
import moe.fuqiuluo.shamrock.remote.service.data.GroupAnnouncementMessageImage import moe.fuqiuluo.shamrock.remote.service.data.GroupAnnouncementMessageImage
import moe.fuqiuluo.shamrock.remote.service.data.push.MemberRole
import moe.fuqiuluo.shamrock.tools.EmptyJsonArray import moe.fuqiuluo.shamrock.tools.EmptyJsonArray
import moe.fuqiuluo.shamrock.tools.GlobalClient import moe.fuqiuluo.shamrock.tools.GlobalClient
import moe.fuqiuluo.shamrock.tools.asInt import moe.fuqiuluo.shamrock.tools.asInt
@ -96,6 +97,8 @@ import java.nio.ByteBuffer
import kotlin.coroutines.resume import kotlin.coroutines.resume
internal object GroupSvc: BaseSvc() { internal object GroupSvc: BaseSvc() {
private const val GET_MEMBER_ROLE_BY_NT = false
private val RefreshTroopMemberInfoLock by lazy { private val RefreshTroopMemberInfoLock by lazy {
Mutex() Mutex()
} }
@ -394,6 +397,27 @@ internal object GroupSvc: BaseSvc() {
.filter { it != 0L } .filter { it != 0L }
} }
suspend fun getMemberRole(groupId: Long, memberUin: Long): MemberRole {
if (!GET_MEMBER_ROLE_BY_NT) {
return when (memberUin) {
getOwner(groupId.toString()) -> MemberRole.Owner
in getAdminList(groupId.toString()) -> MemberRole.Admin
else -> MemberRole.Member
}
}
return when(getTroopMemberInfoByUinViaNt(groupId.toString(), memberUin, 3000).getOrNull()?.role) {
com.tencent.qqnt.kernel.nativeinterface.MemberRole.STRANGER -> MemberRole.Stranger
com.tencent.qqnt.kernel.nativeinterface.MemberRole.MEMBER -> MemberRole.Member
com.tencent.qqnt.kernel.nativeinterface.MemberRole.ADMIN -> MemberRole.Admin
com.tencent.qqnt.kernel.nativeinterface.MemberRole.OWNER -> MemberRole.Owner
com.tencent.qqnt.kernel.nativeinterface.MemberRole.UNSPECIFIED, null -> when (memberUin) {
getOwner(groupId.toString()) -> MemberRole.Owner
in getAdminList(groupId.toString()) -> MemberRole.Admin
else -> MemberRole.Member
}
}
}
fun getOwner(groupId: String): Long { fun getOwner(groupId: String): Long {
val groupInfo = getGroupInfo(groupId) val groupInfo = getGroupInfo(groupId)
return groupInfo.troopowneruin?.toLong() ?: 0 return groupInfo.troopowneruin?.toLong() ?: 0
@ -566,24 +590,53 @@ internal object GroupSvc: BaseSvc() {
} }
} }
private suspend fun getTroopMemberInfoByUinViaNt(groupId: String, qq: Long): Result<MemberInfo> { suspend fun getTroopMemberInfoByUinV2(
val kernelService = NTServiceFetcher.kernelService groupId: String,
val sessionService = kernelService.wrapperSession uin: String,
val groupService = sessionService.groupService refresh: Boolean = false
val info = suspendCancellableCoroutine { ): Result<TroopMemberInfo> {
groupService.getTransferableMemberInfo(groupId.toLong()) { code, _, data -> val service = app.getRuntimeService(ITroopMemberInfoService::class.java, "all")
if (code != 0) { var info = service.getTroopMember(groupId, uin)
it.resume(null) if (refresh || !service.isMemberInCache(groupId, uin) || info == null || info.troopnick == null) {
return@getTransferableMemberInfo info = requestTroopMemberInfo(service, groupId.toLong(), uin.toLong(), timeout = 2000).getOrNull()
}
if (info == null) {
info = getTroopMemberInfoByUinViaNt(groupId, uin.toLong(), timeout = 2000L).getOrNull()?.let {
TroopMemberInfo().apply {
troopnick = it.cardName
friendnick = it.nick
} }
data.forEach { (_, info) -> }
if (info.uin == qq) { }
it.resume(info) try {
return@forEach if (info != null && (info.alias == null || info.alias.isBlank())) {
val req = group_member_info.ReqBody()
req.uint64_group_code.set(groupId.toLong())
req.uint64_uin.set(uin.toLong())
req.bool_new_client.set(true)
req.uint32_client_type.set(1)
req.uint32_rich_card_name_ver.set(1)
val respBuffer = sendBufferAW("group_member_card.get_group_member_card_info", true, req.toByteArray(), timeout = 2000)
if (respBuffer != null) {
val rsp = group_member_info.RspBody()
rsp.mergeFrom(respBuffer.slice(4))
if (rsp.msg_meminfo.str_location.has()) {
info.alias = rsp.msg_meminfo.str_location.get().toStringUtf8()
}
if (rsp.msg_meminfo.uint32_age.has()) {
info.age = rsp.msg_meminfo.uint32_age.get().toByte()
}
if (rsp.msg_meminfo.bytes_group_honor.has()) {
val honorBytes = rsp.msg_meminfo.bytes_group_honor.get().toByteArray()
val honor = troop_honor.GroupUserCardHonor()
honor.mergeFrom(honorBytes)
info.level = honor.level.get()
// 10315: medal_id not real group level
} }
} }
it.resume(null)
} }
} catch (err: Throwable) {
LogCenter.log(err.stackTraceToString(), Level.WARN)
} }
return if (info != null) { return if (info != null) {
Result.success(info) Result.success(info)
@ -592,6 +645,40 @@ internal object GroupSvc: BaseSvc() {
} }
} }
suspend fun getTroopMemberInfoByUinViaNt(
groupId: String,
qq: Long,
timeout: Long = 5000L
): Result<MemberInfo> {
return runCatching {
val kernelService = NTServiceFetcher.kernelService
val sessionService = kernelService.wrapperSession
val groupService = sessionService.groupService
val info = withTimeoutOrNull(timeout) {
suspendCancellableCoroutine {
groupService.getTransferableMemberInfo(groupId.toLong()) { code, _, data ->
if (code != 0) {
it.resume(null)
return@getTransferableMemberInfo
}
data.forEach { (_, info) ->
if (info.uin == qq) {
it.resume(info)
return@forEach
}
}
it.resume(null)
}
}
}
return if (info != null) {
Result.success(info)
} else {
Result.failure(Exception("获取群成员信息失败"))
}
}
}
suspend fun getTroopMemberInfoByUid(groupId: Long, uid: String): Result<MemberInfo> { suspend fun getTroopMemberInfoByUid(groupId: Long, uid: String): Result<MemberInfo> {
val kernelService = NTServiceFetcher.kernelService val kernelService = NTServiceFetcher.kernelService
val sessionService = kernelService.wrapperSession val sessionService = kernelService.wrapperSession
@ -748,7 +835,7 @@ internal object GroupSvc: BaseSvc() {
} }
} }
private suspend fun requestTroopMemberInfo(service: ITroopMemberInfoService, groupId: Long, memberUin: Long): Result<TroopMemberInfo> { private suspend fun requestTroopMemberInfo(service: ITroopMemberInfoService, groupId: Long, memberUin: Long, timeout: Long = 10_000): Result<TroopMemberInfo> {
val info = RefreshTroopMemberInfoLock.withLock { val info = RefreshTroopMemberInfoLock.withLock {
val groupIdStr = groupId.toString() val groupIdStr = groupId.toString()
val memberUinStr = memberUin.toString() val memberUinStr = memberUin.toString()
@ -758,7 +845,7 @@ internal object GroupSvc: BaseSvc() {
requestMemberInfoV2(groupId, memberUin) requestMemberInfoV2(groupId, memberUin)
requestMemberInfo(groupId, memberUin) requestMemberInfo(groupId, memberUin)
withTimeoutOrNull(10000) { withTimeoutOrNull(timeout) {
while (!service.isMemberInCache(groupIdStr, memberUinStr)) { while (!service.isMemberInCache(groupIdStr, memberUinStr)) {
delay(200) delay(200)
} }

View File

@ -5,11 +5,7 @@ package moe.fuqiuluo.qqinterface.servlet
import com.tencent.mobileqq.qroute.QRoute import com.tencent.mobileqq.qroute.QRoute
import com.tencent.mobileqq.troop.api.ITroopMemberNameService import com.tencent.mobileqq.troop.api.ITroopMemberNameService
import com.tencent.qqnt.kernel.api.IKernelService import com.tencent.qqnt.kernel.api.IKernelService
import com.tencent.qqnt.kernel.nativeinterface.IOperateCallback import com.tencent.qqnt.kernel.nativeinterface.*
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
import com.tencent.qqnt.kernel.nativeinterface.TempChatGameSession
import com.tencent.qqnt.kernel.nativeinterface.TempChatPrepareInfo
import com.tencent.qqnt.msg.api.IMsgService import com.tencent.qqnt.msg.api.IMsgService
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
@ -17,21 +13,33 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.decodeFromByteArray
import kotlinx.serialization.encodeToByteArray
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.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.ContactHelper
import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.MessageHelper import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.helper.SendMsgException import moe.fuqiuluo.shamrock.helper.MessageHelper.messageArrayToMessageElements
import moe.fuqiuluo.shamrock.remote.structures.SendMsgResult import moe.fuqiuluo.shamrock.remote.structures.SendMsgResult
import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY import moe.fuqiuluo.shamrock.tools.*
import moe.fuqiuluo.shamrock.utils.DeflateTools
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
import moe.fuqiuluo.shamrock.xposed.helper.msgService import moe.fuqiuluo.shamrock.xposed.helper.msgService
import protobuf.message.*
import protobuf.message.longmsg.*
import tencent.mobileim.structmsg.structmsg.SystemMsg
import java.util.UUID import java.util.UUID
import kotlin.collections.slice
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
import kotlin.random.Random
import kotlin.random.nextLong
internal object MsgSvc: BaseSvc() { internal object MsgSvc : BaseSvc() {
suspend fun prepareTempChatFromGroup( suspend fun prepareTempChatFromGroup(
groupId: String, groupId: String,
peerId: String peerId: String
@ -39,13 +47,19 @@ internal object MsgSvc: BaseSvc() {
LogCenter.log("主动临时消息,创建临时会话。", Level.INFO) LogCenter.log("主动临时消息,创建临时会话。", Level.INFO)
val msgService = app.getRuntimeService(IKernelService::class.java, "all").msgService val msgService = app.getRuntimeService(IKernelService::class.java, "all").msgService
?: return Result.failure(Exception("获取消息服务失败")) ?: return Result.failure(Exception("获取消息服务失败"))
msgService.prepareTempChat(TempChatPrepareInfo( msgService.prepareTempChat(
MsgConstant.KCHATTYPETEMPC2CFROMGROUP, TempChatPrepareInfo(
ContactHelper.getUidByUinAsync(peerId = peerId.toLong()), MsgConstant.KCHATTYPETEMPC2CFROMGROUP,
app.getRuntimeService(ITroopMemberNameService::class.java, "all") ContactHelper.getUidByUinAsync(peerId = peerId.toLong()),
.getTroopMemberNameRemarkFirst(groupId, peerId), app.getRuntimeService(ITroopMemberNameService::class.java, "all")
groupId, EMPTY_BYTE_ARRAY, app.currentUid, "", TempChatGameSession() .getTroopMemberNameRemarkFirst(groupId, peerId),
)) { code, reason -> groupId,
EMPTY_BYTE_ARRAY,
app.currentUid,
"",
TempChatGameSession()
)
) { code, reason ->
if (code != 0) { if (code != 0) {
LogCenter.log("临时会话创建失败: $code, $reason", Level.ERROR) LogCenter.log("临时会话创建失败: $code, $reason", Level.ERROR)
} }
@ -53,6 +67,24 @@ internal object MsgSvc: BaseSvc() {
return Result.success(Unit) return Result.success(Unit)
} }
suspend fun getTempChatInfo(chatType: Int, uid: String): Result<TempChatInfo> {
val msgService = app.getRuntimeService(IKernelService::class.java, "all").msgService
?: return Result.failure(Exception("获取消息服务失败"))
val info: TempChatInfo = withTimeoutOrNull(5000) {
suspendCancellableCoroutine {
msgService.getTempChatInfo(chatType, uid) { code, msg, tempChatInfo ->
if (code == 0) {
it.resume(tempChatInfo)
} else {
LogCenter.log("获取临时会话信息失败: $code:$msg", Level.ERROR)
it.resume(null)
}
}
}
} ?: return Result.failure(Exception("获取临时会话信息失败"))
return Result.success(info)
}
/** /**
* 正常获取 * 正常获取
*/ */
@ -61,7 +93,7 @@ internal object MsgSvc: BaseSvc() {
?: return Result.failure(Exception("没有对应消息映射,消息获取失败")) ?: return Result.failure(Exception("没有对应消息映射,消息获取失败"))
val peerId = mapping.peerId val peerId = mapping.peerId
val contact = MessageHelper.generateContact(mapping.chatType, peerId, mapping.subPeerId ?: "") val contact = MessageHelper.generateContact(mapping.chatType, peerId, mapping.subPeerId)
val msg = withTimeoutOrNull(5000) { val msg = withTimeoutOrNull(5000) {
val service = QRoute.api(IMsgService::class.java) val service = QRoute.api(IMsgService::class.java)
@ -152,7 +184,7 @@ internal object MsgSvc: BaseSvc() {
val mapping = MessageHelper.getMsgMappingByHash(msgHash) val mapping = MessageHelper.getMsgMappingByHash(msgHash)
?: return -1 to "无法找到消息映射" ?: return -1 to "无法找到消息映射"
val contact = MessageHelper.generateContact(mapping.chatType, mapping.peerId, mapping.subPeerId ?: "") val contact = MessageHelper.generateContact(mapping.chatType, mapping.peerId, mapping.subPeerId)
return suspendCancellableCoroutine { continuation -> return suspendCancellableCoroutine { continuation ->
msgService.recallMsg(contact, arrayListOf(mapping.qqMsgId)) { code, why -> msgService.recallMsg(contact, arrayListOf(mapping.qqMsgId)) { code, why ->
@ -182,9 +214,10 @@ internal object MsgSvc: BaseSvc() {
} }
} }
} }
val result = MessageHelper.sendMessageWithoutMsgId(chatType, peedId, message, fromId, MessageCallback(peedId, 0)) val result =
result.onFailure { MessageHelper.sendMessageWithoutMsgId(chatType, peedId, message, fromId, MessageCallback(peedId, 0))
LogCenter.log("sendToAio: " + it.stackTraceToString(), Level.ERROR) if (result.isFailure) {
LogCenter.log("sendToAio: " + result.exceptionOrNull()?.stackTraceToString(), Level.ERROR)
return result return result
} }
val sendResult = result.getOrThrow() val sendResult = result.getOrThrow()
@ -198,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>> { 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 kernelService = NTServiceFetcher.kernelService
val sessionService = kernelService.wrapperSession val sessionService = kernelService.wrapperSession
val msgService = sessionService.msgService val msgService = sessionService.msgService
val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, TicketSvc.getUin()) 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) val msgId = PacketSvc.fakeSelfRecvJsonMsg(msgService, content)
if (msgId < 0) { if (msgId < 0) {
return Result.failure(Exception("获取合并转发消息ID失败")) return Result.failure(Exception("获取合并转发消息ID失败"))
@ -239,7 +313,7 @@ internal object MsgSvc: BaseSvc() {
class MessageCallback( class MessageCallback(
private val peerId: String, private val peerId: String,
var msgHash: Int var msgHash: Int
): IOperateCallback { ) : IOperateCallback {
override fun onResult(code: Int, reason: String?) { override fun onResult(code: Int, reason: String?) {
if (code != 0 && msgHash != 0) { if (code != 0 && msgHash != 0) {
MessageHelper.removeMsgByHashCode(msgHash) MessageHelper.removeMsgByHashCode(msgHash)

View File

@ -15,13 +15,13 @@ import moe.fuqiuluo.shamrock.remote.action.handlers.GetHistoryMsg
import moe.fuqiuluo.shamrock.remote.service.listener.AioListener import moe.fuqiuluo.shamrock.remote.service.listener.AioListener
import moe.fuqiuluo.shamrock.tools.broadcast import moe.fuqiuluo.shamrock.tools.broadcast
import moe.fuqiuluo.shamrock.utils.DeflateTools import moe.fuqiuluo.shamrock.utils.DeflateTools
import protobuf.message.JsonElement import protobuf.message.element.JsonElement
import protobuf.message.MessageBody import protobuf.message.NtMessage
import protobuf.message.MessageContentHead import protobuf.message.MessageContent
import protobuf.message.MessageElement import protobuf.message.MessageElement
import protobuf.message.MessageElementList
import protobuf.message.MessageHead
import protobuf.message.RichMessage import protobuf.message.RichMessage
import protobuf.message.MessageHead
import protobuf.message.MessageBody
import protobuf.push.MessagePush import protobuf.push.MessagePush
import mqq.app.MobileQQ import mqq.app.MobileQQ
import kotlin.coroutines.resume import kotlin.coroutines.resume
@ -51,7 +51,7 @@ internal object PacketSvc: BaseSvc() {
val msgSeq = (latestMsg?.msgSeq ?: 0) + 1 val msgSeq = (latestMsg?.msgSeq ?: 0) + 1
val msgPush = MessagePush( val msgPush = MessagePush(
msgBody = MessageBody( msgBody = NtMessage(
msgHead = MessageHead( msgHead = MessageHead(
peer = app.longAccountUin, peer = app.longAccountUin,
peerUid = app.currentUid, peerUid = app.currentUid,
@ -59,11 +59,11 @@ internal object PacketSvc: BaseSvc() {
receiver = app.longAccountUin, receiver = app.longAccountUin,
receiverUid = app.currentUid receiverUid = app.currentUid
), ),
contentHead = MessageContentHead( contentHead = MessageContent(
msgType = 166, msgType = 166,
msgSubType = 11, msgSubType = 11,
msgSeq = msgSeq, msgSeq = msgSeq,
u1 = msgSeq, msgViaRandom = msgSeq,
msgTime = System.currentTimeMillis() / 1000, msgTime = System.currentTimeMillis() / 1000,
u2 = 1, u2 = 1,
u3 = msgSeq, u3 = msgSeq,
@ -71,7 +71,9 @@ internal object PacketSvc: BaseSvc() {
u4 = msgSeq - 2, u4 = msgSeq - 2,
u5 = msgSeq u5 = msgSeq
), ),
richMsg = RichMessage(MessageElementList(builder())) body = MessageBody(RichMessage(
elements = builder()
))
) )
) )

View File

@ -13,8 +13,11 @@ import tencent.im.oidb.oidb_sso
internal object TicketSvc: BaseSvc() { internal object TicketSvc: BaseSvc() {
object SigType { object SigType {
const val WLOGIN_A2 = 64
const val WLOGIN_A5 = 2 const val WLOGIN_A5 = 2
const val WLOGIN_RESERVED = 16
const val WLOGIN_STWEB = 32 // TLV 103
const val WLOGIN_A2 = 64
const val WLOGIN_ST = 128
const val WLOGIN_AQSIG = 2097152 const val WLOGIN_AQSIG = 2097152
const val WLOGIN_D2 = 262144 const val WLOGIN_D2 = 262144
const val WLOGIN_DA2 = 33554432 const val WLOGIN_DA2 = 33554432
@ -26,14 +29,17 @@ internal object TicketSvc: BaseSvc() {
const val WLOGIN_PSKEY = 1048576 const val WLOGIN_PSKEY = 1048576
const val WLOGIN_PT4Token = 134217728 const val WLOGIN_PT4Token = 134217728
const val WLOGIN_QRPUSH = 67108864 const val WLOGIN_QRPUSH = 67108864
const val WLOGIN_RESERVED = 16
const val WLOGIN_SID = 524288 const val WLOGIN_SID = 524288
const val WLOGIN_SIG64 = 8192 const val WLOGIN_SIG64 = 8192
const val WLOGIN_SKEY = 4096 const val WLOGIN_SKEY = 4096
const val WLOGIN_ST = 128
const val WLOGIN_STWEB = 32 // TLV 103
const val WLOGIN_TOKEN = 32768 const val WLOGIN_TOKEN = 32768
const val WLOGIN_VKEY = 131072 const val WLOGIN_VKEY = 131072
val ALL_TICKET = arrayOf(
WLOGIN_A5, WLOGIN_RESERVED, WLOGIN_STWEB, WLOGIN_A2, WLOGIN_ST, WLOGIN_AQSIG, WLOGIN_D2, WLOGIN_DA2,
WLOGIN_LHSIG, WLOGIN_LSKEY, WLOGIN_OPENKEY, WLOGIN_PAYTOKEN, WLOGIN_PF, WLOGIN_PSKEY, WLOGIN_PT4Token,
WLOGIN_QRPUSH, WLOGIN_SID, WLOGIN_SIG64, WLOGIN_SKEY, WLOGIN_TOKEN, WLOGIN_VKEY
)
} }
fun getUin(): String { fun getUin(): String {
@ -44,6 +50,14 @@ internal object TicketSvc: BaseSvc() {
return app.longAccountUin return app.longAccountUin
} }
fun getUid(): String {
return app.currentUid.ifBlank { "u_" }
}
fun getNickname(): String {
return app.currentNickname
}
fun getCookie(): String { fun getCookie(): String {
val uin = getUin() val uin = getUin()
val skey = getRealSkey(uin) val skey = getRealSkey(uin)

View File

@ -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]
}

View File

@ -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.ArkElement
import com.tencent.qqnt.kernel.nativeinterface.FaceBubbleElement import com.tencent.qqnt.kernel.nativeinterface.FaceBubbleElement
import com.tencent.qqnt.kernel.nativeinterface.FaceElement 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.MarkdownElement
import com.tencent.qqnt.kernel.nativeinterface.MarketFaceElement import com.tencent.qqnt.kernel.nativeinterface.MarketFaceElement
import com.tencent.qqnt.kernel.nativeinterface.MarketFaceSupportSize 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.MessageHelper
import moe.fuqiuluo.shamrock.helper.MusicHelper import moe.fuqiuluo.shamrock.helper.MusicHelper
import moe.fuqiuluo.shamrock.helper.ParamsException import moe.fuqiuluo.shamrock.helper.ParamsException
import moe.fuqiuluo.shamrock.tools.asBoolean
import moe.fuqiuluo.shamrock.tools.asBooleanOrNull import moe.fuqiuluo.shamrock.tools.asBooleanOrNull
import moe.fuqiuluo.shamrock.tools.asInt import moe.fuqiuluo.shamrock.tools.asInt
import moe.fuqiuluo.shamrock.tools.asIntOrNull import moe.fuqiuluo.shamrock.tools.asIntOrNull
import moe.fuqiuluo.shamrock.tools.asJsonArray
import moe.fuqiuluo.shamrock.tools.asJsonObject import moe.fuqiuluo.shamrock.tools.asJsonObject
import moe.fuqiuluo.shamrock.tools.asLong import moe.fuqiuluo.shamrock.tools.asLong
import moe.fuqiuluo.shamrock.tools.asString import moe.fuqiuluo.shamrock.tools.asString
@ -77,40 +82,99 @@ import kotlin.math.roundToInt
import kotlin.random.Random import kotlin.random.Random
import kotlin.random.nextInt import kotlin.random.nextInt
internal typealias IMaker = suspend (Int, Long, String, JsonObject) -> Result<MsgElement> internal typealias IMsgMaker = suspend (Int, Long, String, JsonObject) -> Result<MsgElement>
internal object MessageMaker { internal object MsgElementMaker {
private val makerArray = mutableMapOf( private val makerArray = hashMapOf(
"text" to MessageMaker::createTextElem, "text" to MsgElementMaker::createTextElem,
"face" to MessageMaker::createFaceElem, "face" to MsgElementMaker::createFaceElem,
"pic" to MessageMaker::createImageElem, "pic" to MsgElementMaker::createImageElem,
"image" to MessageMaker::createImageElem, "image" to MsgElementMaker::createImageElem,
"voice" to MessageMaker::createRecordElem, "voice" to MsgElementMaker::createRecordElem,
"record" to MessageMaker::createRecordElem, "record" to MsgElementMaker::createRecordElem,
"at" to MessageMaker::createAtElem, "at" to MsgElementMaker::createAtElem,
"video" to MessageMaker::createVideoElem, "video" to MsgElementMaker::createVideoElem,
"markdown" to MessageMaker::createMarkdownElem, "markdown" to MsgElementMaker::createMarkdownElem,
"dice" to MessageMaker::createDiceElem, "dice" to MsgElementMaker::createDiceElem,
"rps" to MessageMaker::createRpsElem, "rps" to MsgElementMaker::createRpsElem,
"poke" to MessageMaker::createPokeElem, "poke" to MsgElementMaker::createPokeElem,
"anonymous" to MessageMaker::createAnonymousElem, "anonymous" to MsgElementMaker::createAnonymousElem,
"share" to MessageMaker::createShareElem, "share" to MsgElementMaker::createShareElem,
"contact" to MessageMaker::createContactElem, "contact" to MsgElementMaker::createContactElem,
"location" to MessageMaker::createLocationElem, "location" to MsgElementMaker::createLocationElem,
"music" to MessageMaker::createMusicElem, "music" to MsgElementMaker::createMusicElem,
"reply" to MessageMaker::createReplyElem, "reply" to MsgElementMaker::createReplyElem,
"touch" to MessageMaker::createTouchElem, "touch" to MsgElementMaker::createTouchElem,
"weather" to MessageMaker::createWeatherElem, "weather" to MsgElementMaker::createWeatherElem,
"json" to MessageMaker::createJsonElem, "json" to MsgElementMaker::createJsonElem,
"new_dice" to MessageMaker::createNewDiceElem, "new_dice" to MsgElementMaker::createNewDiceElem,
"new_rps" to MessageMaker::createNewRpsElem, "new_rps" to MsgElementMaker::createNewRpsElem,
"basketball" to MessageMaker::createBasketballElem, "basketball" to MsgElementMaker::createBasketballElem,
//"node" to MessageMaker::createNodeElem, //"node" to MessageMaker::createNodeElem,
//"multi_msg" to MessageMaker::createLongMsgStruct, //"multi_msg" to MessageMaker::createLongMsgStruct,
"bubble_face" to MessageMaker::createBubbleFaceElem, "bubble_face" to MsgElementMaker::createBubbleFaceElem,
"inline_keyboard" to MsgElementMaker::createInlineKeywordElem
) )
private suspend fun createBubbleFaceElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> { private suspend fun createInlineKeywordElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
fun tryNewKeyboardButton(btn: JsonObject): InlineKeyboardButton {
return runCatching {
InlineKeyboardButton(
btn["id"].asString,
btn["label"].asString,
btn["visited_label"].asString,
btn["style"].asInt,
btn["type"].asInt,
btn["click_limit"].asInt,
btn["unsupport_tips"].asString,
btn["data"].asString,
btn["at_bot_show_channel_list"].asBoolean,
btn["permission_type"].asInt,
ArrayList(btn["specify_role_ids"].asJsonArray.map { it.asString }),
ArrayList(btn["specify_tinyids"].asJsonArray.map { it.asString }),
false, 0, false, arrayListOf()
)
}.getOrElse {
InlineKeyboardButton(
btn["id"].asString,
btn["label"].asString,
btn["visited_label"].asString,
btn["style"].asInt,
btn["type"].asInt,
btn["click_limit"].asInt,
btn["unsupport_tips"].asString,
btn["data"].asString,
btn["at_bot_show_channel_list"].asBoolean,
btn["permission_type"].asInt,
ArrayList(btn["specify_role_ids"].asJsonArray.map { it.asString }),
ArrayList(btn["specify_tinyids"].asJsonArray.map { it.asString }),
)
}
}
val elem = MsgElement()
elem.elementType = MsgConstant.KELEMTYPEINLINEKEYBOARD
val rows = arrayListOf<InlineKeyboardRow>()
val keyboard = Json.parseToJsonElement(data["data"].asString).asJsonObject
keyboard["rows"].asJsonArray.forEach {
val row = it.asJsonObject
val buttons = arrayListOf<InlineKeyboardButton>()
row["buttons"].asJsonArray.forEach { button ->
val btn = button.asJsonObject
buttons.add(tryNewKeyboardButton(btn))
}
rows.add(InlineKeyboardRow(buttons))
}
elem.inlineKeyboardElement = InlineKeyboardElement(rows, keyboard["bot_appid"].asLong)
return Result.success(elem)
}
private suspend fun createBubbleFaceElem(
chatType: Int,
msgId: Long,
peerId: String,
data: JsonObject
): Result<MsgElement> {
data.checkAndThrow("id", "count") data.checkAndThrow("id", "count")
val faceId = data["id"].asInt val faceId = data["id"].asInt
val local = QQSysFaceUtil.convertToLocal(faceId) val local = QQSysFaceUtil.convertToLocal(faceId)
@ -143,21 +207,13 @@ internal object MessageMaker {
// SendForwardMessage(MsgConstant.KCHATTYPEC2C, TicketSvc.getUin(), data["content"].asJsonArray) // SendForwardMessage(MsgConstant.KCHATTYPEC2C, TicketSvc.getUin(), data["content"].asJsonArray)
// //
// } // }
/**\
* msgElement.setFaceElement(new FaceElement()); private suspend fun createBasketballElem(
* msgElement.getFaceElement().setFaceIndex(114); chatType: Int,
* msgElement.getFaceElement().setFaceText("/篮球"); msgId: Long,
* msgElement.getFaceElement().setFaceType(3); peerId: String,
* msgElement.getFaceElement().setPackId("1"); data: JsonObject
* msgElement.getFaceElement().setStickerId("13"); ): Result<MsgElement> {
* msgElement.getFaceElement().setRandomType(1);
* msgElement.getFaceElement().setImageType(1);
* msgElement.getFaceElement().setStickerType(2);
* msgElement.getFaceElement().setSourceType(1);
* msgElement.getFaceElement().setSurpriseId("");
* msgElement.getFaceElement().setResultId(String.valueOf(new Random().nextInt(5) + 1));
*/
private suspend fun createBasketballElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> {
val elem = MsgElement() val elem = MsgElement()
elem.elementType = MsgConstant.KELEMTYPEFACE elem.elementType = MsgConstant.KELEMTYPEFACE
val face = FaceElement() val face = FaceElement()
@ -168,14 +224,19 @@ internal object MessageMaker {
face.stickerId = "13" face.stickerId = "13"
face.sourceType = 1 face.sourceType = 1
face.stickerType = 2 face.stickerType = 2
face.resultId = Random.nextInt(1 .. 5).toString() face.resultId = Random.nextInt(1..5).toString()
face.surpriseId = "" face.surpriseId = ""
face.randomType = 1 face.randomType = 1
elem.faceElement = face elem.faceElement = face
return Result.success(elem) return Result.success(elem)
} }
private suspend fun createNewRpsElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> { private suspend fun createNewRpsElem(
chatType: Int,
msgId: Long,
peerId: String,
data: JsonObject
): Result<MsgElement> {
val elem = MsgElement() val elem = MsgElement()
elem.elementType = MsgConstant.KELEMTYPEFACE elem.elementType = MsgConstant.KELEMTYPEFACE
val face = FaceElement() val face = FaceElement()
@ -193,7 +254,12 @@ internal object MessageMaker {
return Result.success(elem) return Result.success(elem)
} }
private suspend fun createNewDiceElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> { private suspend fun createNewDiceElem(
chatType: Int,
msgId: Long,
peerId: String,
data: JsonObject
): Result<MsgElement> {
val elem = MsgElement() val elem = MsgElement()
elem.elementType = MsgConstant.KELEMTYPEFACE elem.elementType = MsgConstant.KELEMTYPEFACE
val face = FaceElement() val face = FaceElement()
@ -311,7 +377,7 @@ internal object MessageMaker {
LogCenter.log("无法获取被回复消息", Level.ERROR) LogCenter.log("无法获取被回复消息", Level.ERROR)
} }
if(data.containsKey("text")) { if (data.containsKey("text")) {
data.checkAndThrow("qq", "time", "seq") data.checkAndThrow("qq", "time", "seq")
reply.replayMsgSeq = data["seq"].asLong reply.replayMsgSeq = data["seq"].asLong
reply.sourceMsgText = data["text"].asString reply.sourceMsgText = data["text"].asString
@ -330,21 +396,23 @@ internal object MessageMaker {
): Result<MsgElement> { ): Result<MsgElement> {
data.checkAndThrow("type") data.checkAndThrow("type")
when(val type = data["type"].asString) { when (val type = data["type"].asString) {
"qq" -> { "qq" -> {
data.checkAndThrow("id") data.checkAndThrow("id")
val id = data["id"].asString val id = data["id"].asString
if(!MusicHelper.tryShareQQMusicById(chatType, peerId.toLong(), msgId, id)) { if (!MusicHelper.tryShareQQMusicById(chatType, peerId.toLong(), msgId, id)) {
LogCenter.log("无法发送QQ音乐分享", Level.ERROR) LogCenter.log("无法发送QQ音乐分享", Level.ERROR)
} }
} }
"163" -> { "163" -> {
data.checkAndThrow("id") data.checkAndThrow("id")
val id = data["id"].asString val id = data["id"].asString
if(!MusicHelper.tryShare163MusicById(chatType, peerId.toLong(), msgId, id)) { if (!MusicHelper.tryShare163MusicById(chatType, peerId.toLong(), msgId, id)) {
LogCenter.log("无法发送网易云音乐分享", Level.ERROR) LogCenter.log("无法发送网易云音乐分享", Level.ERROR)
} }
} }
"custom" -> { "custom" -> {
data.checkAndThrow("url", "audio", "title") data.checkAndThrow("url", "audio", "title")
ArkMsgSvc.tryShareMusic( ArkMsgSvc.tryShareMusic(
@ -359,13 +427,19 @@ internal object MessageMaker {
data["audio"].asString data["audio"].asString
) )
} }
else -> LogCenter.log("不支持的音乐分享类型: $type", Level.ERROR) else -> LogCenter.log("不支持的音乐分享类型: $type", Level.ERROR)
} }
return Result.failure(ActionMsgException) return Result.failure(ActionMsgException)
} }
private suspend fun createLocationElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> { private suspend fun createLocationElem(
chatType: Int,
msgId: Long,
peerId: String,
data: JsonObject
): Result<MsgElement> {
data.checkAndThrow("lat", "lon") data.checkAndThrow("lat", "lon")
val lat = data["lat"].asString.toDouble() val lat = data["lat"].asString.toDouble()
@ -378,7 +452,12 @@ internal object MessageMaker {
return Result.failure(ActionMsgException) return Result.failure(ActionMsgException)
} }
private suspend fun createContactElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> { private suspend fun createContactElem(
chatType: Int,
msgId: Long,
peerId: String,
data: JsonObject
): Result<MsgElement> {
data.checkAndThrow("id") data.checkAndThrow("id")
val type = data["type"].asStringOrNull ?: data["kind"].asStringOrNull val type = data["type"].asStringOrNull ?: data["kind"].asStringOrNull
val id = data["id"].asString val id = data["id"].asString
@ -389,10 +468,12 @@ internal object MessageMaker {
val ark = ArkElement(CardSvc.getSharePrivateArkMsg(id.toLong()), null, null) val ark = ArkElement(CardSvc.getSharePrivateArkMsg(id.toLong()), null, null)
elem.arkElement = ark elem.arkElement = ark
} }
"group" -> { "group" -> {
val ark = ArkElement(GroupSvc.getShareTroopArkMsg(id.toLong()), null, null) val ark = ArkElement(GroupSvc.getShareTroopArkMsg(id.toLong()), null, null)
elem.arkElement = ark elem.arkElement = ark
} }
else -> throw IllegalParamsException("type") else -> throw IllegalParamsException("type")
} }
@ -401,7 +482,12 @@ internal object MessageMaker {
return Result.success(elem) return Result.success(elem)
} }
private suspend fun createShareElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> { private suspend fun createShareElem(
chatType: Int,
msgId: Long,
peerId: String,
data: JsonObject
): Result<MsgElement> {
data.checkAndThrow("title", "url") data.checkAndThrow("title", "url")
val url = data["url"].asString val url = data["url"].asString
@ -466,11 +552,21 @@ internal object MessageMaker {
return Result.failure(ActionMsgException) return Result.failure(ActionMsgException)
} }
private suspend fun createAnonymousElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> { private suspend fun createAnonymousElem(
chatType: Int,
msgId: Long,
peerId: String,
data: JsonObject
): Result<MsgElement> {
return Result.failure(ActionMsgException) return Result.failure(ActionMsgException)
} }
private suspend fun createPokeElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> { private suspend fun createPokeElem(
chatType: Int,
msgId: Long,
peerId: String,
data: JsonObject
): Result<MsgElement> {
data.checkAndThrow("type", "id") data.checkAndThrow("type", "id")
val elem = MsgElement() val elem = MsgElement()
val face = FaceElement() val face = FaceElement()
@ -485,8 +581,8 @@ internal object MessageMaker {
face.vaspokeName = "" face.vaspokeName = ""
face.vaspokeMinver = "" face.vaspokeMinver = ""
face.pokeStrength = (data["strength"].asIntOrNull ?: data["cnt"].asIntOrNull face.pokeStrength = (data["strength"].asIntOrNull ?: data["cnt"].asIntOrNull
?: data["count"].asIntOrNull ?: data["time"].asIntOrNull ?: 0).also { ?: data["count"].asIntOrNull ?: data["time"].asIntOrNull ?: 0).also {
if(it < 0 || it > 3) throw IllegalParamsException("strength") if (it < 0 || it > 3) throw IllegalParamsException("strength")
} }
face.msgType = 0 face.msgType = 0
face.faceBubbleCount = 0 face.faceBubbleCount = 0
@ -497,7 +593,12 @@ internal object MessageMaker {
return Result.success(elem) return Result.success(elem)
} }
private suspend fun createFaceElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> { private suspend fun createFaceElem(
chatType: Int,
msgId: Long,
peerId: String,
data: JsonObject
): Result<MsgElement> {
data.checkAndThrow("id") data.checkAndThrow("id")
val serverId = data["id"].asInt val serverId = data["id"].asInt
@ -516,15 +617,15 @@ internal object MessageMaker {
face.faceIndex = serverId face.faceIndex = serverId
face.faceText = QQSysFaceUtil.getFaceDescription(QQSysFaceUtil.convertToLocal(serverId)) face.faceText = QQSysFaceUtil.getFaceDescription(QQSysFaceUtil.convertToLocal(serverId))
if (serverId == 394) { if (serverId == 394) {
face.stickerId = 40.toString() face.stickerId = "40"
face.packId = "1" face.packId = "1"
face.sourceType = 1 face.sourceType = 1
face.stickerType = 3 face.stickerType = 3
face.randomType = 1 face.randomType = 1
face.resultId = data["result"].asStringOrNull ?: Random.nextInt(1 .. 5).toString() face.resultId = data["result"].asStringOrNull ?: Random.nextInt(1..5).toString()
} else if (big) { } else if (big) {
face.imageType = 0 face.imageType = 0
face.stickerId = 30.toString() face.stickerId = "30"
face.packId = "1" face.packId = "1"
face.sourceType = 1 face.sourceType = 1
face.stickerType = 1 face.stickerType = 1
@ -538,7 +639,12 @@ internal object MessageMaker {
return Result.success(elem) return Result.success(elem)
} }
private suspend fun createRpsElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> { private suspend fun createRpsElem(
chatType: Int,
msgId: Long,
peerId: String,
data: JsonObject
): Result<MsgElement> {
val elem = MsgElement() val elem = MsgElement()
elem.elementType = MsgConstant.KELEMTYPEMARKETFACE elem.elementType = MsgConstant.KELEMTYPEMARKETFACE
val market = MarketFaceElement( val market = MarketFaceElement(
@ -553,7 +659,12 @@ internal object MessageMaker {
return Result.success(elem) return Result.success(elem)
} }
private suspend fun createDiceElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> { private suspend fun createDiceElem(
chatType: Int,
msgId: Long,
peerId: String,
data: JsonObject
): Result<MsgElement> {
val elem = MsgElement() val elem = MsgElement()
elem.elementType = MsgConstant.KELEMTYPEMARKETFACE elem.elementType = MsgConstant.KELEMTYPEMARKETFACE
val market = MarketFaceElement( val market = MarketFaceElement(
@ -568,16 +679,26 @@ internal object MessageMaker {
return Result.success(elem) return Result.success(elem)
} }
private suspend fun createMarkdownElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> { private suspend fun createMarkdownElem(
data.checkAndThrow("text") chatType: Int,
msgId: Long,
peerId: String,
data: JsonObject
): Result<MsgElement> {
data.checkAndThrow("content")
val elem = MsgElement() val elem = MsgElement()
elem.elementType = MsgConstant.KELEMTYPEMARKDOWN elem.elementType = MsgConstant.KELEMTYPEMARKDOWN
val markdown = MarkdownElement(data["text"].asString) val markdown = MarkdownElement(data["content"].asString)
elem.markdownElement = markdown elem.markdownElement = markdown
return Result.success(elem) return Result.success(elem)
} }
private suspend fun createVideoElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> { private suspend fun createVideoElem(
chatType: Int,
msgId: Long,
peerId: String,
data: JsonObject
): Result<MsgElement> {
data.checkAndThrow("file") data.checkAndThrow("file")
val file = data["file"].asString.let { val file = data["file"].asString.let {
@ -613,7 +734,8 @@ internal object MessageMaker {
) )
if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize( if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize(
originalPath originalPath
) != file.length()) { ) != file.length()
) {
QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath) QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath)
AudioUtils.obtainVideoCover(file.absolutePath, thumbPath!!) AudioUtils.obtainVideoCover(file.absolutePath, thumbPath!!)
} }
@ -652,35 +774,43 @@ internal object MessageMaker {
val qq = data["qq"].asString val qq = data["qq"].asString
val at = TextElement() val at = TextElement()
when(qq) { when (qq) {
"0", "all" -> { "0", "all" -> {
at.content = "@全体成员" at.content = "@全体成员"
at.atType = MsgConstant.ATTYPEALL at.atType = MsgConstant.ATTYPEALL
at.atNtUid = "0" at.atNtUid = "0"
} }
"online" -> { "online" -> {
at.content = "@在线成员" at.content = "@在线成员"
at.atType = MsgConstant.ATTYPEONLINE at.atType = MsgConstant.ATTYPEONLINE
at.atNtUid = "0" at.atNtUid = "0"
} }
"admin" -> { "admin" -> {
at.content = "@管理员" at.content = "@管理员"
at.atRoleId = 1 at.atRoleId = 1
at.atType = MsgConstant.ATTYPEROLE at.atType = MsgConstant.ATTYPEROLE
at.atNtUid = "0" at.atNtUid = "0"
} }
else -> { else -> {
val info = GroupSvc.getTroopMemberInfoByUin(peerId, qq, true).onFailure { val name = data["name"].asStringOrNull
LogCenter.log("无法获取群成员信息: $qq", Level.ERROR) if (name == null) {
}.getOrNull() val info = GroupSvc.getTroopMemberInfoByUinV2(peerId, qq, true).onFailure {
if (info != null) { LogCenter.log("无法获取群成员信息: $qq", Level.ERROR)
at.content = "@${ }.getOrNull()
info.troopnick if (info != null) {
.ifNullOrEmpty(info.friendnick) at.content = "@${
.ifNullOrEmpty(qq) info.troopnick
}" .ifNullOrEmpty(info.friendnick)
.ifNullOrEmpty(qq)
}"
} else {
at.content = "@$qq"
}
} else { } else {
at.content = "@${data["name"].asStringOrNull.ifNullOrEmpty(qq)}" at.content = "@$name"
} }
at.atType = MsgConstant.ATTYPEONE at.atType = MsgConstant.ATTYPEONE
at.atNtUid = ContactHelper.getUidByUinAsync(qq.toLong()) at.atNtUid = ContactHelper.getUidByUinAsync(qq.toLong())
@ -693,7 +823,12 @@ internal object MessageMaker {
return Result.success(elem) return Result.success(elem)
} }
private suspend fun createRecordElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> { private suspend fun createRecordElem(
chatType: Int,
msgId: Long,
peerId: String,
data: JsonObject
): Result<MsgElement> {
var file = data["file"].asStringOrNull?.let { var file = data["file"].asStringOrNull?.let {
val md5 = it.replace(regex = "[{}\\-]".toRegex(), replacement = "") val md5 = it.replace(regex = "[{}\\-]".toRegex(), replacement = "")
.replace(" ", "") .replace(" ", "")
@ -721,11 +856,13 @@ internal object MessageMaker {
ptt.duration = QRoute.api(IAIOPttApi::class.java) ptt.duration = QRoute.api(IAIOPttApi::class.java)
.getPttFileDuration(file.absolutePath) .getPttFileDuration(file.absolutePath)
} }
MediaType.Amr -> { MediaType.Amr -> {
LogCenter.log({ "Amr: $file" }, Level.DEBUG) LogCenter.log({ "Amr: $file" }, Level.DEBUG)
ptt.duration = AudioUtils.getDurationSec(file) ptt.duration = AudioUtils.getDurationSec(file)
ptt.formatType = MsgConstant.KPTTFORMATTYPEAMR ptt.formatType = MsgConstant.KPTTFORMATTYPEAMR
} }
MediaType.Pcm -> { MediaType.Pcm -> {
LogCenter.log({ "Pcm To Silk: $file" }, Level.DEBUG) LogCenter.log({ "Pcm To Silk: $file" }, Level.DEBUG)
val result = AudioUtils.pcmToSilk(file) val result = AudioUtils.pcmToSilk(file)
@ -733,6 +870,7 @@ internal object MessageMaker {
file = result.first file = result.first
ptt.formatType = MsgConstant.KPTTFORMATTYPESILK ptt.formatType = MsgConstant.KPTTFORMATTYPESILK
} }
else -> { else -> {
LogCenter.log({ "Audio To SILK: $file" }, Level.DEBUG) LogCenter.log({ "Audio To SILK: $file" }, Level.DEBUG)
val result = AudioUtils.audioToSilk(file) val result = AudioUtils.audioToSilk(file)
@ -749,12 +887,13 @@ internal object MessageMaker {
// QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath) // QQNTWrapperUtil.CppProxy.copyFile(file.absolutePath, originalPath)
//} //}
if(!(Transfer with when (chatType) { if (!(Transfer with when (chatType) {
MsgConstant.KCHATTYPEGROUP -> Troop(peerId) MsgConstant.KCHATTYPEGROUP -> Troop(peerId)
MsgConstant.KCHATTYPEC2C -> Private(peerId) MsgConstant.KCHATTYPEC2C -> Private(peerId)
MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> Private(peerId) MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> Private(peerId)
else -> error("Not supported chatType($chatType) for RecordMsg") else -> error("Not supported chatType($chatType) for RecordMsg")
} trans VoiceResource(file))) { } trans VoiceResource(file))
) {
return Result.failure(RuntimeException("上传语音失败: $file")) return Result.failure(RuntimeException("上传语音失败: $file"))
} }
@ -784,7 +923,12 @@ internal object MessageMaker {
return Result.success(elem) return Result.success(elem)
} }
private suspend fun createImageElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> { private suspend fun createImageElem(
chatType: Int,
msgId: Long,
peerId: String,
data: JsonObject
): Result<MsgElement> {
val isOriginal = data["original"].asBooleanOrNull ?: true val isOriginal = data["original"].asBooleanOrNull ?: true
val isFlash = data["flash"].asBooleanOrNull ?: false val isFlash = data["flash"].asBooleanOrNull ?: false
val filePath = data["file"].asStringOrNull val filePath = data["file"].asStringOrNull
@ -825,7 +969,8 @@ internal object MessageMaker {
) )
if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize( if (!QQNTWrapperUtil.CppProxy.fileIsExist(originalPath) || QQNTWrapperUtil.CppProxy.getFileSize(
originalPath originalPath
) != file.length()) { ) != file.length()
) {
val thumbPath = msgService.getRichMediaFilePathForMobileQQSend( val thumbPath = msgService.getRichMediaFilePathForMobileQQSend(
RichMediaFilePathInfo( RichMediaFilePathInfo(
2, 0, pic.md5HexStr, file.name, 2, 720, null, "", true 2, 0, pic.md5HexStr, file.name, 2, 720, null, "", true
@ -863,7 +1008,12 @@ internal object MessageMaker {
return Result.success(elem) return Result.success(elem)
} }
private suspend fun createTextElem(chatType: Int, msgId: Long, peerId: String, data: JsonObject): Result<MsgElement> { private suspend fun createTextElem(
chatType: Int,
msgId: Long,
peerId: String,
data: JsonObject
): Result<MsgElement> {
data.checkAndThrow("text") data.checkAndThrow("text")
val elem = MsgElement() val elem = MsgElement()
elem.elementType = MsgConstant.KELEMTYPETEXT elem.elementType = MsgConstant.KELEMTYPETEXT
@ -879,5 +1029,5 @@ internal object MessageMaker {
} }
} }
operator fun get(type: String): IMaker? = makerArray[type] operator fun get(type: String): IMsgMaker? = makerArray[type]
} }

View File

@ -58,6 +58,7 @@ internal object MessageConvert {
//MsgConstant.KELEMTYPEMULTIFORWARD to XmlMultiMsgConverter, //MsgConstant.KELEMTYPEMULTIFORWARD to XmlMultiMsgConverter,
//MsgConstant.KELEMTYPESTRUCTLONGMSG to XmlLongMsgConverter, //MsgConstant.KELEMTYPESTRUCTLONGMSG to XmlLongMsgConverter,
MsgConstant.KELEMTYPEFACEBUBBLE to BubbleFaceConverter, MsgConstant.KELEMTYPEFACEBUBBLE to BubbleFaceConverter,
MsgConstant.KELEMTYPEINLINEKEYBOARD to InlineKeyboardConverter,
) )
} }

View File

@ -2,6 +2,10 @@ package moe.fuqiuluo.qqinterface.servlet.msg.convert
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgElement 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.qqinterface.servlet.transfile.RichProtoSvc
import moe.fuqiuluo.shamrock.helper.ContactHelper import moe.fuqiuluo.shamrock.helper.ContactHelper
import moe.fuqiuluo.shamrock.helper.Level 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.helper.db.MessageDB
import moe.fuqiuluo.shamrock.tools.asJsonObject import moe.fuqiuluo.shamrock.tools.asJsonObject
import moe.fuqiuluo.shamrock.tools.asString import moe.fuqiuluo.shamrock.tools.asString
import moe.fuqiuluo.shamrock.tools.hex2ByteArray
import moe.fuqiuluo.shamrock.tools.json import moe.fuqiuluo.shamrock.tools.json
internal sealed class MessageElemConverter: IMessageConvert { internal sealed class MessageElemConverter: IMessageConvert {
@ -135,14 +140,19 @@ internal sealed class MessageElemConverter: IMessageConvert {
ImageMapping(md5.uppercase(), chatType, image.fileSize) ImageMapping(md5.uppercase(), chatType, image.fileSize)
) )
//LogCenter.log(image.toString())
val originalUrl = image.originImageUrl ?: ""
//LogCenter.log({ "receive image: $image" }, Level.DEBUG)
return MessageSegment( return MessageSegment(
type = "image", type = "image",
data = hashMapOf( data = hashMapOf(
"file" to md5, "file" to md5,
"url" to when(chatType) { "url" to when(chatType) {
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(md5) MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(originalUrl, md5)
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(md5) MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(originalUrl, md5)
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(md5) MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(originalUrl, md5)
else -> unknownChatType(chatType) else -> unknownChatType(chatType)
}, },
"subType" to image.picSubType, "subType" to image.picSubType,
@ -201,7 +211,15 @@ internal sealed class MessageElemConverter: IMessageConvert {
element: MsgElement element: MsgElement
): MessageSegment { ): MessageSegment {
val video = element.videoElement 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( return MessageSegment(
type = "video", type = "video",
@ -210,7 +228,7 @@ internal sealed class MessageElemConverter: IMessageConvert {
"url" to when(chatType) { "url" to when(chatType) {
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid) MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CVideoDownUrl("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) else -> unknownChatType(chatType)
} }
).also { ).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) { protected fun unknownChatType(chatType: Int) {
throw UnsupportedOperationException("Not supported chat type: $chatType") throw UnsupportedOperationException("Not supported chat type: $chatType")
} }

View File

@ -19,18 +19,35 @@ import moe.fuqiuluo.shamrock.tools.slice
import moe.fuqiuluo.shamrock.tools.toHexString import moe.fuqiuluo.shamrock.tools.toHexString
import moe.fuqiuluo.shamrock.utils.PlatformUtils import moe.fuqiuluo.shamrock.utils.PlatformUtils
import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
import mqq.app.MobileQQ
import protobuf.oidb.cmd0xfc2.Oidb0xfc2ChannelInfo import protobuf.oidb.cmd0xfc2.Oidb0xfc2ChannelInfo
import protobuf.oidb.cmd0xfc2.Oidb0xfc2MsgApplyDownloadReq import protobuf.oidb.cmd0xfc2.Oidb0xfc2MsgApplyDownloadReq
import protobuf.oidb.cmd0xfc2.Oidb0xfc2ReqBody import protobuf.oidb.cmd0xfc2.Oidb0xfc2ReqBody
import protobuf.oidb.cmd0xfc2.Oidb0xfc2RspBody import protobuf.oidb.cmd0xfc2.Oidb0xfc2RspBody
import mqq.app.MobileQQ
import tencent.im.cs.cmd0x346.cmd0x346 import tencent.im.cs.cmd0x346.cmd0x346
import tencent.im.oidb.cmd0x6d6.oidb_0x6d6 import tencent.im.oidb.cmd0x6d6.oidb_0x6d6
import tencent.im.oidb.cmd0xe37.cmd0xe37 import tencent.im.oidb.cmd0xe37.cmd0xe37
import tencent.im.oidb.oidb_sso import tencent.im.oidb.oidb_sso
import kotlin.coroutines.resume import kotlin.coroutines.resume
private const val GPRO_PIC = "gchat.qpic.cn"
private const val GPRO_PIC_NT = "multimedia.nt.qq.com.cn"
private const val C2C_PIC = "c2cpicdw.qpic.cn"
internal object RichProtoSvc: BaseSvc() { internal object RichProtoSvc: BaseSvc() {
var multiMediaRKey = "CAQSKAB6JWENi5LMk0kc62l8Pm3Jn1dsLZHyRLAnNmHGoZ3y_gDZPqZt-64"
/*@Deprecated("Use RichProtoSvc.getQQDns instead", ReplaceWith("getQQDns(domain)"))
fun getQQDns(domain: String) {
val bundle = Bundle()
bundle.putString("domain", "xxx")
bundle.putInt("businessType", 1)
val result = BinderMethodProxy
.callServer(QIPCClientHelper.getInstance().client, "InnerDnsModule", "reqDomain2IpList", bundle)
if (result.isSuccess) {
val ipList: ArrayList<IpData> = result.data.getParcelableArrayList("ip")!!
}
}*/
suspend fun getGuildFileDownUrl(peerId: String, channelId: String, fileId: String, bizId: Int): String { suspend fun getGuildFileDownUrl(peerId: String, channelId: String, fileId: String, bizId: Int): String {
val buffer = sendOidbAW("OidbSvcTrpcTcp.0xfc2_0", 4034, 0, ProtoBuf.encodeToByteArray( val buffer = sendOidbAW("OidbSvcTrpcTcp.0xfc2_0", 4034, 0, ProtoBuf.encodeToByteArray(
Oidb0xfc2ReqBody( Oidb0xfc2ReqBody(
@ -142,24 +159,48 @@ internal object RichProtoSvc: BaseSvc() {
} }
fun getGroupPicDownUrl( fun getGroupPicDownUrl(
md5: String originalUrl: String,
md5: String,
): String { ): String {
return "http://gchat.qpic.cn/gchatpic_new/0/0-0-${md5.uppercase()}/0?term=2" val isNtServer = originalUrl.startsWith("/download")
val domain = if (isNtServer) GPRO_PIC_NT else GPRO_PIC
if (originalUrl.isNotEmpty()) {
if (isNtServer && !originalUrl.contains("rkey=")) {
return "https://$domain$originalUrl&rkey=$multiMediaRKey"
}
return "https://$domain$originalUrl"
}
return "https://$domain/gchatpic_new/0/0-0-${md5.uppercase()}/0?term=2"
} }
fun getC2CPicDownUrl( fun getC2CPicDownUrl(
originalUrl: String,
md5: String md5: String
): String { ): String {
return "https://c2cpicdw.qpic.cn/offpic_new/0/123-0-${md5.uppercase()}/0?term=2" 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 { fun getGuildPicDownUrl(
return "https://gchat.qpic.cn/qmeetpic/0/0-0-${md5.uppercase()}/0?term=2" originalUrl: String,
md5: String
): String {
val isNtServer = originalUrl.startsWith("/download")
val domain = if (isNtServer) GPRO_PIC_NT else GPRO_PIC
if (originalUrl.isNotEmpty()) {
if (isNtServer && !originalUrl.contains("rkey=")) {
return "https://$domain$originalUrl&rkey=$multiMediaRKey"
}
return "https://$domain$originalUrl"
}
return "https://$domain/qmeetpic/0/0-0-${md5.uppercase()}/0?term=2"
} }
suspend fun getC2CVideoDownUrl( suspend fun getC2CVideoDownUrl(
peerId: String, peerId: String,
md5Hex: String, md5: ByteArray,
fileUUId: String fileUUId: String
): String { ): String {
return suspendCancellableCoroutine { return suspendCancellableCoroutine {
@ -175,7 +216,7 @@ internal object RichProtoSvc: BaseSvc() {
downReq.troopUin = peerId downReq.troopUin = peerId
downReq.clientType = 2 downReq.clientType = 2
downReq.fileId = fileUUId downReq.fileId = fileUUId
downReq.md5 = md5Hex.hex2ByteArray() downReq.md5 = md5
downReq.busiType = FileTransfer.BUSI_TYPE_SHORT_VIDEO downReq.busiType = FileTransfer.BUSI_TYPE_SHORT_VIDEO
downReq.subBusiType = 0 downReq.subBusiType = 0
downReq.fileType = FileTransfer.VIDEO_FORMAT_MP4 downReq.fileType = FileTransfer.VIDEO_FORMAT_MP4
@ -202,7 +243,7 @@ internal object RichProtoSvc: BaseSvc() {
suspend fun getGroupVideoDownUrl( suspend fun getGroupVideoDownUrl(
peerId: String, peerId: String,
md5Hex: String, md5: ByteArray,
fileUUId: String fileUUId: String
): String { ): String {
return suspendCancellableCoroutine { return suspendCancellableCoroutine {
@ -218,7 +259,7 @@ internal object RichProtoSvc: BaseSvc() {
downReq.troopUin = peerId downReq.troopUin = peerId
downReq.clientType = 2 downReq.clientType = 2
downReq.fileId = fileUUId downReq.fileId = fileUUId
downReq.md5 = md5Hex.hex2ByteArray() downReq.md5 = md5
downReq.busiType = FileTransfer.BUSI_TYPE_SHORT_VIDEO downReq.busiType = FileTransfer.BUSI_TYPE_SHORT_VIDEO
downReq.subBusiType = 0 downReq.subBusiType = 0
downReq.fileType = FileTransfer.VIDEO_FORMAT_MP4 downReq.fileType = FileTransfer.VIDEO_FORMAT_MP4
@ -321,12 +362,4 @@ internal object RichProtoSvc: BaseSvc() {
RichProtoProc.procRichProtoReq(richProtoReq) RichProtoProc.procRichProtoReq(richProtoReq)
} }
} }
suspend fun getGuildPttDownUrl(
peerId: String,
md5Hex: String,
fileUUId: String
): String {
return "unsupported"
}
} }

View File

@ -6,6 +6,7 @@ import com.tencent.mobileqq.transfile.TransferRequest
import moe.fuqiuluo.shamrock.utils.MD5 import moe.fuqiuluo.shamrock.utils.MD5
import java.io.File import java.io.File
import moe.fuqiuluo.qqinterface.servlet.transfile.ResourceType.* import moe.fuqiuluo.qqinterface.servlet.transfile.ResourceType.*
import moe.fuqiuluo.shamrock.helper.TransfileHelper
internal object Transfer: FileTransfer() { internal object Transfer: FileTransfer() {
private val ROUTE = mapOf<ContactType, Map<ResourceType, suspend TransTarget.(Resource) -> Boolean>>( private val ROUTE = mapOf<ContactType, Map<ResourceType, suspend TransTarget.(Resource) -> Boolean>>(
@ -84,11 +85,14 @@ internal object Transfer: FileTransfer() {
file: File, file: File,
wait: Boolean = true wait: Boolean = true
): Boolean { ): Boolean {
return transC2CResource(peerId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_SHARE, wait) { return transC2CResource(peerId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_CAMERA, wait) {
val picUpExtraInfo = TransferRequest.PicUpExtraInfo() val picUpExtraInfo = TransferRequest.PicUpExtraInfo()
picUpExtraInfo.mIsRaw = true picUpExtraInfo.mIsRaw = false
picUpExtraInfo.mUinType = FileMsg.UIN_BUDDY
it.mPicSendSource = 8 it.mPicSendSource = 8
it.mExtraObj = picUpExtraInfo it.mExtraObj = picUpExtraInfo
it.mIsPresend = true
it.delayShowProgressTimeInMs = 2000
} }
} }
@ -97,10 +101,13 @@ internal object Transfer: FileTransfer() {
file: File, file: File,
wait: Boolean = true wait: Boolean = true
): Boolean { ): Boolean {
return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_SHARE, wait) { return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_CAMERA, wait) {
val picUpExtraInfo = TransferRequest.PicUpExtraInfo() val picUpExtraInfo = TransferRequest.PicUpExtraInfo()
picUpExtraInfo.mIsRaw = true //picUpExtraInfo.mIsRaw = !TransfileHelper.isGifFile(file)
picUpExtraInfo.mIsRaw = false
picUpExtraInfo.mUinType = FileMsg.UIN_TROOP
it.mPicSendSource = 8 it.mPicSendSource = 8
it.delayShowProgressTimeInMs = 2000
it.mExtraObj = picUpExtraInfo it.mExtraObj = picUpExtraInfo
} }
} }

View File

@ -2,7 +2,7 @@ package moe.fuqiuluo.shamrock.helper
internal abstract class InternalMessageMakerError(why: String): RuntimeException(why) internal abstract class InternalMessageMakerError(why: String): RuntimeException(why)
internal class ParamsException(key: String): InternalMessageMakerError("Lack of param $key") internal class ParamsException(key: String): InternalMessageMakerError("Lack of param `$key`")
internal class IllegalParamsException(key: String): InternalMessageMakerError("Illegal param $key") internal class IllegalParamsException(key: String): InternalMessageMakerError("Illegal param $key")

View File

@ -16,7 +16,8 @@ import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import moe.fuqiuluo.qqinterface.servlet.MsgSvc import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.qqinterface.servlet.msg.MessageMaker import moe.fuqiuluo.qqinterface.servlet.msg.MessageElementMaker
import moe.fuqiuluo.qqinterface.servlet.msg.MsgElementMaker
import moe.fuqiuluo.shamrock.helper.db.MessageDB import moe.fuqiuluo.shamrock.helper.db.MessageDB
import moe.fuqiuluo.shamrock.helper.db.MessageMapping import moe.fuqiuluo.shamrock.helper.db.MessageMapping
import moe.fuqiuluo.shamrock.remote.structures.SendMsgResult import moe.fuqiuluo.shamrock.remote.structures.SendMsgResult
@ -26,8 +27,8 @@ import moe.fuqiuluo.shamrock.tools.asJsonObjectOrNull
import moe.fuqiuluo.shamrock.tools.asString import moe.fuqiuluo.shamrock.tools.asString
import moe.fuqiuluo.shamrock.tools.json import moe.fuqiuluo.shamrock.tools.json
import moe.fuqiuluo.shamrock.tools.jsonArray import moe.fuqiuluo.shamrock.tools.jsonArray
import protobuf.message.MessageElement
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.math.abs import kotlin.math.abs
internal object MessageHelper { internal object MessageHelper {
@ -39,7 +40,7 @@ internal object MessageHelper {
fromId: String = peerId fromId: String = peerId
): SendMsgResult { ): SendMsgResult {
val uniseq = generateMsgId(chatType) val uniseq = generateMsgId(chatType)
val msg = messageArrayToMessageElements(chatType, uniseq.qqMsgId, peerId, decodeCQCode(message)).also { val msg = messageArrayToMsgElements(chatType, uniseq.qqMsgId, peerId, decodeCQCode(message)).also {
if (it.second.isEmpty() && !it.first) { if (it.second.isEmpty() && !it.first) {
error("消息合成失败,请查看日志或者检查输入。") error("消息合成失败,请查看日志或者检查输入。")
} else if (it.second.isEmpty()) { } else if (it.second.isEmpty()) {
@ -82,7 +83,7 @@ internal object MessageHelper {
callback: IOperateCallback callback: IOperateCallback
): Result<SendMsgResult> { ): Result<SendMsgResult> {
val uniseq = generateMsgId(chatType) val uniseq = generateMsgId(chatType)
val msg = messageArrayToMessageElements(chatType, uniseq.qqMsgId, peerId, message).also { val msg = messageArrayToMsgElements(chatType, uniseq.qqMsgId, peerId, message).also {
if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。") if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。")
}.second.filter { }.second.filter {
it.elementType != -1 it.elementType != -1
@ -166,7 +167,7 @@ internal object MessageHelper {
fromId: String = peerId fromId: String = peerId
): SendMsgResult { ): SendMsgResult {
val uniseq = generateMsgId(chatType) val uniseq = generateMsgId(chatType)
val msg = messageArrayToMessageElements(chatType, uniseq.qqMsgId, peerId, message).also { val msg = messageArrayToMsgElements(chatType, uniseq.qqMsgId, peerId, message).also {
if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。") if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。")
}.second.filter { }.second.filter {
it.elementType != -1 it.elementType != -1
@ -224,7 +225,7 @@ internal object MessageHelper {
fromId: String = peerId fromId: String = peerId
): SendMsgResult { ): SendMsgResult {
val uniseq = generateMsgId(chatType) val uniseq = generateMsgId(chatType)
val msg = messageArrayToMessageElements(chatType, uniseq.qqMsgId, peerId, message).also { val msg = messageArrayToMsgElements(chatType, uniseq.qqMsgId, peerId, message).also {
if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。") if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。")
}.second.filter { }.second.filter {
it.elementType != -1 it.elementType != -1
@ -233,7 +234,7 @@ internal object MessageHelper {
return if (!message.isEmpty()) { return if (!message.isEmpty()) {
val service = QRoute.api(IMsgService::class.java) val service = QRoute.api(IMsgService::class.java)
return suspendCancellableCoroutine { return suspendCancellableCoroutine {
service.sendMsg(contact, uniseq.qqMsgId, msg) { code, why -> service.sendMsg(contact, uniseq.qqMsgId, msg) { _, _ ->
it.resume(uniseq.copy(msgTime = System.currentTimeMillis())) it.resume(uniseq.copy(msgTime = System.currentTimeMillis()))
} }
} }
@ -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>() val msgList = arrayListOf<MsgElement>()
var hasActionMsg = false var hasActionMsg = false
messageList.forEach { messageList.forEach {
val msg = it.jsonObject val msg = it.jsonObject
val maker = MessageMaker[msg["type"].asString] val maker = MsgElementMaker[msg["type"].asString]
if (maker != null) {
try {
val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject
maker(chatType, msgId, targetUin, data).onSuccess { msgElem ->
msgList.add(msgElem)
}.onFailure {
if (it.javaClass != ActionMsgException::class.java) {
throw it
} else {
hasActionMsg = true
}
}
} catch (e: Throwable) {
LogCenter.log(e.stackTraceToString(), Level.ERROR)
}
} else {
LogCenter.log("不支持的消息类型: ${msg["type"].asString}", Level.ERROR)
return false to arrayListOf()
}
}
return hasActionMsg to msgList
}
suspend fun messageArrayToMessageElements(chatType: Int, msgId: Long, targetUin: String, messageList: JsonArray): Pair<Boolean, ArrayList<MessageElement>> {
val msgList = arrayListOf<MessageElement>()
var hasActionMsg = false
messageList.forEach {
val msg = it.jsonObject
val maker = MessageElementMaker[msg["type"].asString]
if (maker != null) { if (maker != null) {
try { try {
val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject
@ -353,6 +383,21 @@ internal object MessageHelper {
database.messageMappingDao().insert(mapping) database.messageMappingDao().insert(mapping)
} }
fun saveMsgMappingNotExist(
hash: Int,
qqMsgId: Long,
time: Long,
chatType: Int,
peerId: String,
subPeerId: String,
msgSeq: Int,
subChatType: Int = chatType
) {
val database = MessageDB.getInstance()
val mapping = MessageMapping(hash, qqMsgId, chatType, subChatType, peerId, time, msgSeq, subPeerId)
database.messageMappingDao().insertNotExist(mapping)
}
external fun createMessageUniseq(chatType: Int, time: Long): Long external fun createMessageUniseq(chatType: Int, time: Long): Long
fun decodeCQCode(code: String): JsonArray { fun decodeCQCode(code: String): JsonArray {

View File

@ -1,6 +1,8 @@
package moe.fuqiuluo.shamrock.helper package moe.fuqiuluo.shamrock.helper
import java.io.File import java.io.File
import java.io.IOException
import java.io.RandomAccessFile
internal object TransfileHelper { internal object TransfileHelper {
private val extensionMap = mapOf( private val extensionMap = mapOf(
@ -94,4 +96,15 @@ internal object TransfileHelper {
val extension = name.substring(index) val extension = name.substring(index)
return extensionMap[extension] ?: -1 return extensionMap[extension] ?: -1
} }
fun isGifFile(picFile: File): Boolean {
if (picFile.exists() && picFile.length() > 3) {
return RandomAccessFile(picFile, "r").use {
val bArr = ByteArray(3)
it.read(bArr)
if (bArr[0].toInt() == 71 && bArr[1].toInt() == 73 && bArr[2].toInt() == 70) { return true } else false
}
}
return false
}
} }

View File

@ -30,6 +30,9 @@ interface MessageMappingDao {
@Insert(onConflict = OnConflictStrategy.REPLACE) @Insert(onConflict = OnConflictStrategy.REPLACE)
fun insert(mapping: MessageMapping) fun insert(mapping: MessageMapping)
@Insert(onConflict = OnConflictStrategy.ABORT)
fun insertNotExist(mapping: MessageMapping)
@Query("UPDATE message_mapping_v2 SET msgSeq = :msgSeq WHERE msgHashId = :hash") @Query("UPDATE message_mapping_v2 SET msgSeq = :msgSeq WHERE msgHashId = :hash")
fun updateMsgSeqByMsgHash(hash: Int, msgSeq: Int) fun updateMsgSeqByMsgHash(hash: Int, msgSeq: Int)

View File

@ -1,9 +1,12 @@
package moe.fuqiuluo.shamrock.remote.action.handlers package moe.fuqiuluo.shamrock.remote.action.handlers
import com.tencent.guild.api.transfile.IGuildTransFileApi
import com.tencent.mobileqq.qroute.QRoute
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.qqinterface.servlet.TicketSvc import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import moe.fuqiuluo.shamrock.remote.action.ActionSession import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.remote.service.data.BigDataTicket
import moe.fuqiuluo.shamrock.remote.service.data.Credentials import moe.fuqiuluo.shamrock.remote.service.data.Credentials
import moe.fuqiuluo.shamrock.tools.EmptyJsonString import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.symbols.OneBotHandler import moe.fuqiuluo.symbols.OneBotHandler
@ -17,10 +20,20 @@ internal object GetCookies: IActionHandler() {
} }
operator fun invoke(echo: JsonElement = EmptyJsonString): String { operator fun invoke(echo: JsonElement = EmptyJsonString): String {
return ok(Credentials(cookie = TicketSvc.getCookie()), echo) return ok(Credentials(
cookie = TicketSvc.getCookie(),
bigDataTicket = QRoute.api(IGuildTransFileApi::class.java).bigDataTicket?.let {
BigDataTicket(it.sessionKey, it.sessionSig)
}
), echo)
} }
suspend operator fun invoke(domain: String, echo: JsonElement = EmptyJsonString): String { suspend operator fun invoke(domain: String, echo: JsonElement = EmptyJsonString): String {
return ok(Credentials(cookie = TicketSvc.getCookie(domain)), echo) return ok(Credentials(
cookie = TicketSvc.getCookie(domain),
bigDataTicket = QRoute.api(IGuildTransFileApi::class.java).bigDataTicket?.let {
BigDataTicket(it.sessionKey, it.sessionSig)
}
), echo)
} }
} }

View File

@ -1,5 +1,6 @@
package moe.fuqiuluo.shamrock.remote.action.handlers package moe.fuqiuluo.shamrock.remote.action.handlers
import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
@ -18,8 +19,10 @@ internal object GetGProChannelList: IActionHandler() {
return invoke(guildId.toULong(), refresh, echo = session.echo) return invoke(guildId.toULong(), refresh, echo = session.echo)
} }
operator fun invoke(guildId: ULong, refresh: Boolean, echo: JsonElement = EmptyJsonString): String { suspend operator fun invoke(guildId: ULong, refresh: Boolean, echo: JsonElement = EmptyJsonString): String {
val result = GProSvc.getChannelList(guildId, refresh) val result = withTimeoutOrNull(5000) {
GProSvc.getChannelList(guildId, refresh)
} ?: return error("timeout", echo)
result.onFailure { result.onFailure {
return error(it.message ?: "unable to fetch channel list", echo) return error(it.message ?: "unable to fetch channel list", echo)
} }

View File

@ -59,6 +59,16 @@ internal object GetHistoryMsg: IActionHandler() {
val msgList = ArrayList<MessageDetail>().apply { val msgList = ArrayList<MessageDetail>().apply {
addAll(result.data!!.map { msg -> addAll(result.data!!.map { msg ->
val msgHash = MessageHelper.generateMsgIdHash(msg.chatType, msg.msgId) val msgHash = MessageHelper.generateMsgIdHash(msg.chatType, msg.msgId)
MessageHelper.saveMsgMappingNotExist(
hash = msgHash,
qqMsgId = msg.msgId,
chatType = msg.chatType,
subChatType = msg.chatType,
peerId = peerId,
msgSeq = msg.msgSeq.toInt(),
time = msg.msgTime,
subPeerId = msg.channelId ?: peerId
)
MessageDetail( MessageDetail(
time = msg.msgTime.toInt(), time = msg.msgTime.toInt(),
msgType = MessageHelper.obtainDetailTypeByMsgType(msg.chatType), msgType = MessageHelper.obtainDetailTypeByMsgType(msg.chatType),

View File

@ -35,8 +35,8 @@ internal object GetImage: IActionHandler() {
image.size, image.size,
image.fileName, image.fileName,
when(image.chatType) { when(image.chatType) {
MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(fileMd5) MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl("", fileMd5)
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(fileMd5) MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl("", fileMd5)
else -> error("Not supported chat type: ${image.chatType}, convertMsgElementsToMsgSegment::Pic") else -> error("Not supported chat type: ${image.chatType}, convertMsgElementsToMsgSegment::Pic")
} }
), echo = echo) ), echo = echo)

View File

@ -52,11 +52,7 @@ internal object GetTroopMemberInfo : IActionHandler() {
area = info.alias ?: "", area = info.alias ?: "",
lastSentTime = info.last_active_time, lastSentTime = info.last_active_time,
level = info.level, level = info.level,
role = when { role = GroupSvc.getMemberRole(groupId.toLong(), uin.toLong()),
GroupSvc.getOwner(groupId).toString() == uin -> MemberRole.Owner
uin.toLong() in GroupSvc.getAdminList(groupId) -> MemberRole.Admin
else -> MemberRole.Member
},
unfriendly = false, unfriendly = false,
title = info.mUniqueTitle ?: "", title = info.mUniqueTitle ?: "",
titleExpireTime = info.mUniqueTitleExpire, titleExpireTime = info.mUniqueTitleExpire,

View File

@ -54,12 +54,13 @@ internal object GetTroopMemberList : IActionHandler() {
area = info.alias ?: "", area = info.alias ?: "",
lastSentTime = info.last_active_time, lastSentTime = info.last_active_time,
level = info.level, level = info.level,
role = when { role = GroupSvc.getMemberRole(groupId.toLong(), info.memberuin.toLong())
/*when {
GroupSvc.getOwner(groupId) GroupSvc.getOwner(groupId)
.toString() == info.memberuin -> MemberRole.Owner .toString() == info.memberuin -> MemberRole.Owner
info.memberuin.toLong() in GroupSvc.getAdminList(groupId) -> MemberRole.Admin info.memberuin.toLong() in GroupSvc.getAdminList(groupId) -> MemberRole.Admin
else -> MemberRole.Member else -> MemberRole.Member
}, }*/,
unfriendly = false, unfriendly = false,
title = info.mUniqueTitle ?: "", title = info.mUniqueTitle ?: "",
titleExpireTime = info.mUniqueTitleExpire, titleExpireTime = info.mUniqueTitleExpire,

View File

@ -2,6 +2,7 @@ package moe.fuqiuluo.shamrock.remote.action.handlers
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.qqinterface.servlet.GroupSvc import moe.fuqiuluo.qqinterface.servlet.GroupSvc
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import moe.fuqiuluo.shamrock.remote.action.ActionSession import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.tools.EmptyJsonString import moe.fuqiuluo.shamrock.tools.EmptyJsonString
@ -17,7 +18,7 @@ internal object ModifyTroopMemberName: IActionHandler() {
} }
operator fun invoke(groupId: String, userId: String, card: String, echo: JsonElement = EmptyJsonString): String { operator fun invoke(groupId: String, userId: String, card: String, echo: JsonElement = EmptyJsonString): String {
if (!GroupSvc.isAdmin(groupId)) { if (!GroupSvc.isAdmin(groupId) && userId != TicketSvc.getUin()) {
return logic("you are not admin", echo) return logic("you are not admin", echo)
} }
return if(GroupSvc.modifyGroupMemberCard(groupId.toLong(), userId.toLong(), card)) return if(GroupSvc.modifyGroupMemberCard(groupId.toLong(), userId.toLong(), card))

View File

@ -1,7 +1,6 @@
package moe.fuqiuluo.shamrock.remote.action.handlers package moe.fuqiuluo.shamrock.remote.action.handlers
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MultiMsgInfo
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
import moe.fuqiuluo.qqinterface.servlet.MsgSvc import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.qqinterface.servlet.TicketSvc import moe.fuqiuluo.qqinterface.servlet.TicketSvc
@ -14,8 +13,11 @@ import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.remote.service.data.ForwardMessageResult import moe.fuqiuluo.shamrock.remote.service.data.ForwardMessageResult
import moe.fuqiuluo.shamrock.tools.* import moe.fuqiuluo.shamrock.tools.*
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
import moe.fuqiuluo.symbols.OneBotHandler import moe.fuqiuluo.symbols.OneBotHandler
import protobuf.message.*
import protobuf.message.longmsg.PushMsgBody
import java.util.*
import kotlin.random.Random
@OneBotHandler("send_forward_msg") @OneBotHandler("send_forward_msg")
internal object SendForwardMessage : IActionHandler() { internal object SendForwardMessage : IActionHandler() {
@ -37,14 +39,26 @@ internal object SendForwardMessage : IActionHandler() {
return noParam("detail_type/message_type", session.echo) return noParam("detail_type/message_type", session.echo)
} }
} }
val peerId = when(chatType) { val peerId = when (chatType) {
MsgConstant.KCHATTYPEGROUP -> session.getStringOrNull("group_id") ?: return noParam("group_id", session.echo) MsgConstant.KCHATTYPEGROUP -> session.getStringOrNull("group_id") ?: return noParam(
MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getStringOrNull("user_id") ?: return noParam("user_id", session.echo) "group_id",
session.echo
)
MsgConstant.KCHATTYPEC2C, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getStringOrNull("user_id")
?: return noParam("user_id", session.echo)
else -> error("unknown chat type: $chatType") else -> error("unknown chat type: $chatType")
} }
val fromId = when(chatType) { val fromId = when (chatType) {
MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getStringOrNull("group_id") ?: return noParam("group_id", session.echo) MsgConstant.KCHATTYPEGROUP, MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> session.getStringOrNull("group_id")
MsgConstant.KCHATTYPEC2C -> session.getStringOrNull("user_id") ?: return noParam("user_id", session.echo) ?: return noParam("group_id", session.echo)
MsgConstant.KCHATTYPEC2C -> session.getStringOrNull("user_id") ?: return noParam(
"user_id",
session.echo
)
else -> error("unknown chat type: $chatType") else -> error("unknown chat type: $chatType")
} }
return if (session.isArray("messages")) { return if (session.isArray("messages")) {
@ -68,103 +82,184 @@ internal object SendForwardMessage : IActionHandler() {
echo: JsonElement = EmptyJsonString echo: JsonElement = EmptyJsonString
): String { ): String {
kotlin.runCatching { kotlin.runCatching {
val kernelService = NTServiceFetcher.kernelService var uid: String? = null
val sessionService = kernelService.wrapperSession var groupUin: String? = null
val msgService = sessionService.msgService
val selfUin = TicketSvc.getUin()
val multiNodes = messages.map { var i = -1
if (it.asJsonObject["type"].asStringOrNull != "node") { val desc = MutableList(messages.size) { "" }
LogCenter.log("包含非node类型节点", Level.WARN)
return@map null val msgs = messages.map { msg ->
} val data = msg.asJsonObject["data"].asJsonObject
if (it.asJsonObject["data"] !is JsonObject) { if (data.containsKey("id")) {
LogCenter.log("data字段错误", Level.WARN) val record = MsgSvc.getMsg(data["id"].asInt).getOrElse {
return@map null LogCenter.log("合并转发消息节点消息(id = ${data["id"].asInt})获取失败:$it", Level.WARN)
}
it.asJsonObject["data"].asJsonObject.let { data ->
if (data.containsKey("id")) {
val record = MsgSvc.getMsg(data["id"].asInt).getOrNull()
if (record == null) {
LogCenter.log("合并转发消息节点消息获取失败:${data["id"]}", Level.WARN)
return@map null
} else {
record.peerName to record.toSegments().map { segment ->
segment.toJson()
}.json
}
} else if (data.containsKey("content")) {
(data["name"].asStringOrNull ?: "Anno") to when (val raw = data["content"]) {
is JsonObject -> raw.asJsonArray
is JsonArray -> raw.asJsonArray
else -> MessageHelper.decodeCQCode(raw.asString)
}
} else {
LogCenter.log("消息节点缺少id或content字段", Level.WARN)
return@map null return@map null
} }
}.let { node -> uid = record.peerUid
val content = node.second.map { msg -> if (record.chatType == MsgConstant.KCHATTYPEGROUP) groupUin = record.peerUin.toString()
when (msg.asJsonObject["type"].asStringOrNull ?: "text") { PushMsgBody(
"at" -> { head = MessageHead(
buildJsonObject { peerUid = record.senderUid,
put("type", "text") groupInfo = if (record.chatType == MsgConstant.KCHATTYPEGROUP) GroupInfo(
putJsonObject("data") { groupCode = record.peerUin.toULong(),
put( memberCard = record.sendMemberName,
"text", "@${ u1 = 2
msg.asJsonObject["data"].asJsonObject["name"].asStringOrNull.ifNullOrEmpty( ) else null
msg.asJsonObject["data"].asJsonObject["qq"].asString ),
) 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] += "[合并转发消息]"
}
} }
} ).also {
} if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。")
}.second
"voice" -> { )
buildJsonObject { )
put("type", "text") )
putJsonObject("data") { } else {
put("text", "[语音]") LogCenter.log("消息节点缺少id或content字段", Level.WARN)
} null
}
}
"node" -> {
LogCenter.log("合并转发消息暂时不支持嵌套", Level.WARN)
buildJsonObject {
put("type", "text")
putJsonObject("data") {
put("text", "[合并转发消息]")
}
}
}
else -> msg
}
}.json
val result = MessageHelper.sendMessageNoCb(MsgConstant.KCHATTYPEC2C, selfUin, content)
if (result.qqMsgId == 0L) {
LogCenter.log("合并转发消息节点消息发送失败", Level.WARN)
return@map null
}
result.qqMsgId to node.first
} }
}.filterNotNull() }.filterNotNull().ifEmpty { return logic("消息节点为空", echo) }
val from = MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, selfUin)
val to = MessageHelper.generateContact(chatType, peerId, fromId)
val uniseq = MessageHelper.generateMsgId(chatType) val resid = MsgSvc.sendMultiMsg(uid ?: TicketSvc.getUid(), groupUin, msgs)
msgService.multiForwardMsg(ArrayList<MultiMsgInfo>().apply { .getOrElse { return logic(it.message ?: "", echo) }
multiNodes.forEach { add(MultiMsgInfo(it.first, it.second)) } val uniseq = UUID.randomUUID().toString().uppercase()
}.also { it.reverse() }, from, to, MsgSvc.MessageCallback(peerId, uniseq.msgHashId))
val result = MsgSvc.sendToAio(
chatType, peerId,
listOf(
hashMapOf(
"type" to "json", "data" to hashMapOf(
"data" to hashMapOf(
"app" to "com.tencent.multimsg",
"config" to hashMapOf(
"autosize" to 1,
"forward" to 1,
"round" to 1,
"type" to "normal",
"width" to 300
).json,
"desc" to "[聊天记录]",
"extra" to hashMapOf(
"filename" to uniseq,
"tsum" to 2
).json.toString() + "\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( return ok(
ForwardMessageResult( ForwardMessageResult(
msgId = uniseq.msgHashId, msgId = result.msgHashId,
forwardId = "" forwardId = resid
), echo = echo ), echo = echo
) )
}.onFailure { }.onFailure {

View File

@ -120,18 +120,14 @@ internal object SendMessage: IActionHandler() {
//if (!ContactHelper.checkContactAvailable(chatType, peerId)) { //if (!ContactHelper.checkContactAvailable(chatType, peerId)) {
// return logic("contact is not found", echo = echo) // return logic("contact is not found", echo = echo)
//} //}
val result = MsgSvc.sendToAio(chatType, peerId, message, fromId = fromId, retryCnt) val result = MsgSvc.sendToAio(chatType, peerId, message, fromId = fromId, retryCnt).getOrElse { return logic(it.message ?: "", echo) }
if (result.isFailure) { if (result.msgHashId <= 0) {
return logic(result.exceptionOrNull()?.message ?: "", echo)
}
val sendMsgResult = result.getOrThrow()
if (sendMsgResult.msgHashId <= 0) {
return logic("send message failed", echo = echo) return logic("send message failed", echo = echo)
} }
recallDuration?.let { autoRecall(sendMsgResult.msgHashId, it) } recallDuration?.let { autoRecall(result.msgHashId, it) }
return ok(MessageResult( return ok(MessageResult(
msgId = sendMsgResult.msgHashId, msgId = result.msgHashId,
time = (sendMsgResult.msgTime * 0.001).toLong() time = (result.msgTime * 0.001).toLong()
), echo) ), echo)
} }

View File

@ -103,7 +103,7 @@ internal object UploadGroupFile : IActionHandler() {
// 根据文件大小调整超时时间 // 根据文件大小调整超时时间
val msgIdPair = MessageHelper.generateMsgId(MsgConstant.KCHATTYPEGROUP) val msgIdPair = MessageHelper.generateMsgId(MsgConstant.KCHATTYPEGROUP)
val info = (withTimeoutOrNull((srcFile.length() / (300 * 1024)) * 1000 + 5000) { val info = (withTimeoutOrNull((srcFile.length() / (125 * 1024)) * 1000 + 5000) {
val msgService = QRoute.api(IMsgService::class.java) val msgService = QRoute.api(IMsgService::class.java)
val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, groupId) val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, groupId)
suspendCancellableCoroutine<FileTransNotifyInfo?> { suspendCancellableCoroutine<FileTransNotifyInfo?> {

View File

@ -5,6 +5,7 @@ import com.tencent.mobileqq.transfile.TransferRequest
import com.tencent.mobileqq.transfile.api.ITransFileController import com.tencent.mobileqq.transfile.api.ITransFileController
import io.ktor.server.routing.Routing import io.ktor.server.routing.Routing
import io.ktor.server.routing.post import io.ktor.server.routing.post
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
import moe.fuqiuluo.shamrock.remote.structures.Status import moe.fuqiuluo.shamrock.remote.structures.Status
import moe.fuqiuluo.shamrock.tools.fetchPost import moe.fuqiuluo.shamrock.tools.fetchPost
import moe.fuqiuluo.shamrock.tools.respond import moe.fuqiuluo.shamrock.tools.respond
@ -15,7 +16,7 @@ import kotlin.random.Random
import kotlin.random.nextLong import kotlin.random.nextLong
fun Routing.registerBDH() { fun Routing.registerBDH() {
post("/upload_group_image") { if(ShamrockConfig.isDev()) post("/upload_group_image") {
val troop = fetchPost("troop") val troop = fetchPost("troop")
val picBytes = Base64.decode(fetchPost("pic"), Base64.DEFAULT) val picBytes = Base64.decode(fetchPost("pic"), Base64.DEFAULT)
val md5Str = MD5.getMd5Hex(picBytes) val md5Str = MD5.getMd5Hex(picBytes)
@ -46,5 +47,4 @@ fun Routing.registerBDH() {
.transferAsync(transferRequest) .transferAsync(transferRequest)
respond(isOk = true, Status.Ok, "$md5Str.jpg") respond(isOk = true, Status.Ok, "$md5Str.jpg")
} }
} }

View File

@ -5,7 +5,9 @@ import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import io.ktor.server.application.call import io.ktor.server.application.call
import io.ktor.server.response.respondText import io.ktor.server.response.respondText
import io.ktor.server.routing.Routing import io.ktor.server.routing.Routing
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.shamrock.remote.action.handlers.* import moe.fuqiuluo.shamrock.remote.action.handlers.*
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
import moe.fuqiuluo.shamrock.remote.structures.Status import moe.fuqiuluo.shamrock.remote.structures.Status
import moe.fuqiuluo.shamrock.tools.* import moe.fuqiuluo.shamrock.tools.*
@ -44,20 +46,36 @@ fun Routing.ticketActions() {
} }
} }
fun getTicket(uin: String, id: Int, debug: Boolean = false) = TicketSvc.getTicket(uin, id)?.let {
mutableMapOf(
"sig" to (it._sig?.toHexString() ?: "null"),
"key" to (it._sig_key?.toHexString() ?: "null")
).also { map ->
if (debug)
map["content"] = ((it._sig?.decodeToString() ?: "") + ":" + (it._sig_key?.decodeToString() ?: "null"))
}.json.asJsonObject
} ?: EmptyJsonObject
getOrPost("/get_ticket") { getOrPost("/get_ticket") {
val uin = fetchOrThrow("uin") val uin = fetchOrThrow("uin")
val ticket = when(val id = fetchOrThrow("id").toInt()) { val ticket = when(val id = fetchOrThrow("id").toInt()) {
32 -> TicketSvc.getStWeb(uin) 32 -> TicketSvc.getStWeb(uin)
else -> { else -> {
respond(true, Status.Ok, data = TicketSvc.getTicket(uin, id)?.let { respond(true, Status.Ok, data = getTicket(uin, id))
mapOf(
"sig" to (it._sig?.toHexString() ?: "null"),
"key" to (it._sig_key?.toHexString() ?: "null")
).json.asJsonObject
} ?: EmptyJsonObject)
return@getOrPost return@getOrPost
} }
} }
respond(true, Status.Ok, data = ticket) respond(true, Status.Ok, data = ticket)
} }
if (ShamrockConfig.isDev()) getOrPost("/get_all_ticket") {
val uin = fetchOrThrow("uin")
val ticketMap = mutableMapOf<Int, JsonElement>()
TicketSvc.SigType.ALL_TICKET.forEach {
ticketMap[it] = getTicket(uin, it, true)
}
respond(true, Status.Ok, data = ticketMap)
}
} }

View File

@ -26,35 +26,33 @@ import moe.fuqiuluo.shamrock.remote.action.handlers.QuickOperation.quicklyReply
import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter
internal object HttpService: HttpTransmitServlet() { internal object HttpService: HttpTransmitServlet() {
private val jobList = arrayListOf<Job>() private val subscribes = arrayListOf<Job>()
override fun submitFlowJob(job: Job) { override fun subscribe(job: Job) {
// HTTP 回调不会触发断连无需释放之前的JOB subscribes.add(job)
jobList.add(job)
} }
override fun cancelFlowJobs() { override fun unsubscribe() {
jobList.removeIf { subscribes.removeIf {
it.cancel() it.cancel()
return@removeIf true return@removeIf true
} }
} }
override fun initTransmitter() { override fun init() {
if (jobList.isNotEmpty()) return if (subscribes.isNotEmpty()) return
submitFlowJob(GlobalScope.launch { subscribe(GlobalScope.launch {
GlobalEventTransmitter.onMessageEvent { (record, event) -> GlobalEventTransmitter.onMessageEvent { (record, event) ->
val respond = pushTo(event) ?: return@onMessageEvent val respond = pushTo(event) ?: return@onMessageEvent
handleQuicklyReply(record, event.messageId, respond.bodyAsText()) handleQuicklyReply(record, event.messageId, respond.bodyAsText())
} }
}) })
submitFlowJob(GlobalScope.launch { subscribe(GlobalScope.launch {
GlobalEventTransmitter.onNoticeEvent { event -> GlobalEventTransmitter.onNoticeEvent { event ->
pushTo(event) pushTo(event)
} }
}) })
submitFlowJob(GlobalScope.launch { subscribe(GlobalScope.launch {
GlobalEventTransmitter.onRequestEvent { GlobalEventTransmitter.onRequestEvent {
pushTo(it) pushTo(it)
} }

View File

@ -16,28 +16,28 @@ internal class WebSocketClientService(
heartbeatInterval: Long, heartbeatInterval: Long,
wsHeaders: Map<String, String> wsHeaders: Map<String, String>
) : WebSocketClientServlet(address, heartbeatInterval, wsHeaders) { ) : WebSocketClientServlet(address, heartbeatInterval, wsHeaders) {
private val eventJobList = mutableSetOf<Job>() private val subscribes = mutableSetOf<Job>()
init { init {
startHeartbeatTimer() startHeartbeatTimer()
} }
override fun submitFlowJob(job: Job) { override fun subscribe(job: Job) {
eventJobList.add(job) subscribes.add(job)
} }
override fun initTransmitter() { override fun init() {
submitFlowJob(GlobalScope.launch { subscribe(GlobalScope.launch {
GlobalEventTransmitter.onMessageEvent { (_, event) -> GlobalEventTransmitter.onMessageEvent { (_, event) ->
pushTo(event) pushTo(event)
} }
}) })
submitFlowJob(GlobalScope.launch { subscribe(GlobalScope.launch {
GlobalEventTransmitter.onNoticeEvent { event -> GlobalEventTransmitter.onNoticeEvent { event ->
pushTo(event) pushTo(event)
} }
}) })
submitFlowJob(GlobalScope.launch { subscribe(GlobalScope.launch {
GlobalEventTransmitter.onRequestEvent { event -> GlobalEventTransmitter.onRequestEvent { event ->
pushTo(event) pushTo(event)
} }
@ -45,8 +45,8 @@ internal class WebSocketClientService(
LogCenter.log("WebSocketClientService: 初始化服务", Level.WARN) LogCenter.log("WebSocketClientService: 初始化服务", Level.WARN)
} }
override fun cancelFlowJobs() { override fun unsubscribe() {
eventJobList.removeIf { job -> subscribes.removeIf { job ->
job.cancel() job.cancel()
return@removeIf true return@removeIf true
} }

View File

@ -26,24 +26,24 @@ internal class WebSocketService(
port: Int, port: Int,
heartbeatInterval: Long, heartbeatInterval: Long,
): WebSocketTransmitServlet(host, port, heartbeatInterval) { ): WebSocketTransmitServlet(host, port, heartbeatInterval) {
private val eventJobList = mutableSetOf<Job>() private val subscribes = mutableSetOf<Job>()
override fun submitFlowJob(job: Job) { override fun subscribe(job: Job) {
eventJobList.add(job) subscribes.add(job)
} }
override fun initTransmitter() { override fun init() {
submitFlowJob(GlobalScope.launch { subscribe(GlobalScope.launch {
GlobalEventTransmitter.onMessageEvent { (_, event) -> GlobalEventTransmitter.onMessageEvent { (_, event) ->
pushTo(event) pushTo(event)
} }
}) })
submitFlowJob(GlobalScope.launch { subscribe(GlobalScope.launch {
GlobalEventTransmitter.onNoticeEvent { event -> GlobalEventTransmitter.onNoticeEvent { event ->
pushTo(event) pushTo(event)
} }
}) })
submitFlowJob(GlobalScope.launch { subscribe(GlobalScope.launch {
GlobalEventTransmitter.onRequestEvent { event -> GlobalEventTransmitter.onRequestEvent { event ->
pushTo(event) pushTo(event)
} }
@ -51,8 +51,8 @@ internal class WebSocketService(
LogCenter.log("WebSocketService: 初始化服务", Level.WARN) LogCenter.log("WebSocketService: 初始化服务", Level.WARN)
} }
override fun cancelFlowJobs() { override fun unsubscribe() {
eventJobList.removeIf { job -> subscribes.removeIf { job ->
job.cancel() job.cancel()
return@removeIf true return@removeIf true
} }
@ -60,8 +60,10 @@ internal class WebSocketService(
} }
override fun onOpen(conn: WebSocket, handshake: ClientHandshake) { override fun onOpen(conn: WebSocket, handshake: ClientHandshake) {
val token = ShamrockConfig.getActiveWebSocketConfig()?.token ?: ShamrockConfig.getToken() val token = ShamrockConfig.getActiveWebSocketConfig()?.tokens
if (token.isNotBlank()) { ?: ShamrockConfig.getActiveWebSocketConfig()?.token?.split(",", "|", "")
?: listOf(ShamrockConfig.getToken())
if (token.isNotEmpty()) {
var accessToken = handshake.getFieldValue("access_token") var accessToken = handshake.getFieldValue("access_token")
.ifNullOrEmpty(handshake.getFieldValue("ticket")) .ifNullOrEmpty(handshake.getFieldValue("ticket"))
.ifNullOrEmpty(handshake.getFieldValue("Authorization")) .ifNullOrEmpty(handshake.getFieldValue("Authorization"))
@ -69,8 +71,7 @@ internal class WebSocketService(
if (accessToken.startsWith("Bearer ", ignoreCase = true)) { if (accessToken.startsWith("Bearer ", ignoreCase = true)) {
accessToken = accessToken.substring(7) accessToken = accessToken.substring(7)
} }
val tokenList = token.split(",", "|", "") if (!token.contains(accessToken)) {
if (!tokenList.contains(accessToken)) {
conn.close() conn.close()
LogCenter.log({ "WSServer连接错误(${conn.remoteSocketAddress.address.hostAddress}:${conn.remoteSocketAddress.port}) 没有提供正确的token, $accessToken" }, Level.ERROR) LogCenter.log({ "WSServer连接错误(${conn.remoteSocketAddress.address.hostAddress}:${conn.remoteSocketAddress.port}) 没有提供正确的token, $accessToken" }, Level.ERROR)
return return

View File

@ -1,24 +1,20 @@
package moe.fuqiuluo.shamrock.remote.service.api package moe.fuqiuluo.shamrock.remote.service.api
import com.tencent.mobileqq.app.QQAppInterface import com.tencent.mobileqq.app.QQAppInterface
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import moe.fuqiuluo.shamrock.remote.service.data.push.NoticeSubType
import moe.fuqiuluo.shamrock.remote.service.data.push.NoticeType
import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
import oicq.wlogin_sdk.tools.MD5 import oicq.wlogin_sdk.tools.MD5
internal interface BaseTransmitServlet { internal interface BaseTransmitServlet {
val address: String val address: String
fun allowTransmit(): Boolean fun transmitAccess(): Boolean
fun submitFlowJob(job: Job) fun subscribe(job: Job)
fun cancelFlowJobs() fun unsubscribe()
fun initTransmitter() fun init()
val app: QQAppInterface val app: QQAppInterface
get() = AppRuntimeFetcher.appRuntime as QQAppInterface get() = AppRuntimeFetcher.appRuntime as QQAppInterface

View File

@ -1,9 +1,15 @@
@file:OptIn(DelicateCoroutinesApi::class)
package moe.fuqiuluo.shamrock.remote.service.api package moe.fuqiuluo.shamrock.remote.service.api
import com.tencent.qqnt.kernel.nativeinterface.MsgElement import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.channels.BufferOverflow
import kotlinx.coroutines.flow.FlowCollector import kotlinx.coroutines.flow.FlowCollector
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.launch
import moe.fuqiuluo.qqinterface.servlet.BaseSvc import moe.fuqiuluo.qqinterface.servlet.BaseSvc
import moe.fuqiuluo.qqinterface.servlet.CardSvc import moe.fuqiuluo.qqinterface.servlet.CardSvc
import moe.fuqiuluo.qqinterface.servlet.GroupSvc import moe.fuqiuluo.qqinterface.servlet.GroupSvc
@ -48,7 +54,7 @@ internal object GlobalEventTransmitter: BaseSvc() {
private suspend fun transMessageEvent(record: MsgRecord, message: MessageEvent) = messageEventFlow.emit(record to message) private suspend fun transMessageEvent(record: MsgRecord, message: MessageEvent) = messageEventFlow.emit(record to message)
/** /**
* 消息 手淫器 * 消息
*/ */
object MessageTransmitter { object MessageTransmitter {
/** /**
@ -86,11 +92,11 @@ internal object GlobalEventTransmitter: BaseSvc() {
.ifEmpty { record.sendMemberName } .ifEmpty { record.sendMemberName }
.ifEmpty { record.peerName }, .ifEmpty { record.peerName },
card = record.sendMemberName, card = record.sendMemberName,
role = when (record.senderUin) { role = GroupSvc.getMemberRole(record.peerUin, record.senderUin)/*when (record.senderUin) {
GroupSvc.getOwner(record.peerUin.toString()) -> MemberRole.Owner GroupSvc.getOwner(record.peerUin.toString()) -> MemberRole.Owner
in GroupSvc.getAdminList(record.peerUin.toString()) -> MemberRole.Admin in GroupSvc.getAdminList(record.peerUin.toString()) -> MemberRole.Admin
else -> MemberRole.Member else -> MemberRole.Member
}, }*/,
title = "", title = "",
level = "", level = "",
) )
@ -108,7 +114,9 @@ internal object GlobalEventTransmitter: BaseSvc() {
rawMsg: String, rawMsg: String,
msgHash: Int, msgHash: Int,
postType: PostType, postType: PostType,
tempSource: MessageTempSource = MessageTempSource.Unknown tempSource: MessageTempSource = MessageTempSource.Unknown,
groupId: Long = Long.MIN_VALUE,
fromNick: String? = null
): Boolean { ): Boolean {
val botUin = app.longAccountUin val botUin = app.longAccountUin
var nickName = record.sendNickName var nickName = record.sendNickName
@ -142,7 +150,9 @@ internal object GlobalEventTransmitter: BaseSvc() {
title = "", title = "",
level = "", level = "",
), ),
tmpSource = tempSource.id tmpSource = tempSource.id,
groupId = groupId,
fromNickName = fromNick
) )
) )
return true return true
@ -172,6 +182,8 @@ internal object GlobalEventTransmitter: BaseSvc() {
postType = postType, postType = postType,
messageType = MsgType.Guild, messageType = MsgType.Guild,
subType = MsgSubType.Channel, subType = MsgSubType.Channel,
guildId = record.guildId,
channelId = record.channelId,
messageId = msgHash, messageId = msgHash,
targetId = record.peerUin, targetId = record.peerUin,
peerId = botUin, peerId = botUin,
@ -186,7 +198,7 @@ internal object GlobalEventTransmitter: BaseSvc() {
userId = record.senderUid.toLong(), userId = record.senderUid.toLong(),
nickname = nickName, nickname = nickName,
card = record.sendMemberName, card = record.sendMemberName,
role = MemberRole.Member, role = MemberRole.Member, // TODO(GUILD ROLE)
title = record.sendNickName, title = record.sendNickName,
level = record.roleId.toString(), level = record.roleId.toString(),
tinyId = record.senderUid tinyId = record.senderUid
@ -554,19 +566,29 @@ internal object GlobalEventTransmitter: BaseSvc() {
@ShamrockDsl @ShamrockDsl
suspend inline fun onMessageEvent(collector: FlowCollector<Pair<MsgRecord, MessageEvent>>) { suspend inline fun onMessageEvent(collector: FlowCollector<Pair<MsgRecord, MessageEvent>>) {
messageEventFlow.collect(collector) messageEventFlow.collect {
GlobalScope.launch {
collector.emit(it)
}
}
} }
@ShamrockDsl @ShamrockDsl
suspend inline fun onNoticeEvent(collector: FlowCollector<NoticeEvent>) { suspend inline fun onNoticeEvent(collector: FlowCollector<NoticeEvent>) {
noticeEventFlow noticeEventFlow.collect {
.collect(collector) GlobalScope.launch {
collector.emit(it)
}
}
} }
@ShamrockDsl @ShamrockDsl
suspend inline fun onRequestEvent(collector: FlowCollector<RequestEvent>) { suspend inline fun onRequestEvent(collector: FlowCollector<RequestEvent>) {
requestEventFlow requestEventFlow.collect {
.collect(collector) GlobalScope.launch {
collector.emit(it)
}
}
} }
} }

View File

@ -8,7 +8,6 @@ import io.ktor.client.request.setBody
import io.ktor.client.statement.HttpResponse import io.ktor.client.statement.HttpResponse
import io.ktor.http.ContentType import io.ktor.http.ContentType
import io.ktor.http.contentType import io.ktor.http.contentType
import kotlinx.coroutines.Job
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
import moe.fuqiuluo.shamrock.tools.GlobalClient import moe.fuqiuluo.shamrock.tools.GlobalClient
import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.Level
@ -22,12 +21,12 @@ import java.net.SocketException
internal abstract class HttpTransmitServlet : BaseTransmitServlet { internal abstract class HttpTransmitServlet : BaseTransmitServlet {
override val address: String by lazy { ShamrockConfig.getWebHookAddress() } override val address: String by lazy { ShamrockConfig.getWebHookAddress() }
override fun allowTransmit(): Boolean { override fun transmitAccess(): Boolean {
return ShamrockConfig.allowWebHook() return ShamrockConfig.allowWebHook()
} }
protected suspend inline fun <reified T> pushTo(body: T): HttpResponse? { protected suspend inline fun <reified T> pushTo(body: T): HttpResponse? {
if (!allowTransmit()) return null if (!transmitAccess()) return null
try { try {
if (address.startsWith("http://") || address.startsWith("https://")) { if (address.startsWith("http://") || address.startsWith("https://")) {
val response = GlobalClient.post(address) { val response = GlobalClient.post(address) {

View File

@ -45,7 +45,7 @@ internal abstract class WebSocketClientServlet(
private var firstOpen = true private var firstOpen = true
private val sendLock = Mutex() private val sendLock = Mutex()
override fun allowTransmit(): Boolean { override fun transmitAccess(): Boolean {
return ShamrockConfig.openWebSocketClient() return ShamrockConfig.openWebSocketClient()
} }
@ -90,9 +90,9 @@ internal abstract class WebSocketClientServlet(
if (firstOpen) { if (firstOpen) {
firstOpen = false firstOpen = false
} else { } else {
cancelFlowJobs() unsubscribe()
} }
initTransmitter() init()
} }
override fun onClose(code: Int, reason: String?, remote: Boolean) { override fun onClose(code: Int, reason: String?, remote: Boolean) {
@ -105,18 +105,18 @@ internal abstract class WebSocketClientServlet(
} }
} }
LogCenter.log("WebSocketClient onClose: $code, $reason, $remote") LogCenter.log("WebSocketClient onClose: $code, $reason, $remote")
cancelFlowJobs() unsubscribe()
connectedClients.remove(url) connectedClients.remove(url)
} }
override fun onError(ex: Exception?) { override fun onError(ex: Exception?) {
LogCenter.log("WebSocketClient onError: ${ex?.message}") LogCenter.log("WebSocketClient onError: ${ex?.message}")
cancelFlowJobs() unsubscribe()
connectedClients.remove(url) connectedClients.remove(url)
} }
protected suspend inline fun <reified T> pushTo(body: T) { protected suspend inline fun <reified T> pushTo(body: T) {
if (!allowTransmit() || isClosed || isClosing) return if (!transmitAccess() || isClosed || isClosing) return
try { try {
sendLock.withLock { sendLock.withLock {
send(GlobalJson.encodeToString(body)) send(GlobalJson.encodeToString(body))

View File

@ -41,10 +41,14 @@ internal abstract class WebSocketTransmitServlet(
private val sendLock = Mutex() private val sendLock = Mutex()
protected val eventReceivers: MutableList<WebSocket> = Collections.synchronizedList(mutableListOf<WebSocket>()) protected val eventReceivers: MutableList<WebSocket> = Collections.synchronizedList(mutableListOf<WebSocket>())
init {
connectionLostTimeout = 0
}
override val address: String override val address: String
get() = "-" get() = "-"
override fun allowTransmit(): Boolean { override fun transmitAccess(): Boolean {
return ShamrockConfig.openWebSocket() return ShamrockConfig.openWebSocket()
} }
@ -125,16 +129,16 @@ internal abstract class WebSocketTransmitServlet(
override fun onError(conn: WebSocket, ex: Exception?) { override fun onError(conn: WebSocket, ex: Exception?) {
LogCenter.log("WSServer Error: " + ex?.stackTraceToString(), Level.ERROR) LogCenter.log("WSServer Error: " + ex?.stackTraceToString(), Level.ERROR)
cancelFlowJobs() unsubscribe()
} }
override fun onStart() { override fun onStart() {
LogCenter.log("WSServer start running on ws://${getAddress()}!") LogCenter.log("WSServer start running on ws://${getAddress()}!")
initTransmitter() init()
} }
protected suspend inline fun <reified T> pushTo(body: T) { protected suspend inline fun <reified T> pushTo(body: T) {
if(!allowTransmit()) return if(!transmitAccess()) return
try { try {
sendLock.withLock { sendLock.withLock {
broadcastTextEvent(GlobalJson.encodeToString(body)) broadcastTextEvent(GlobalJson.encodeToString(body))

View File

@ -18,6 +18,7 @@ data class ConnectionConfig(
@SerialName("address") val address: String? = null, @SerialName("address") val address: String? = null,
@SerialName("port") var port: Int? = null, @SerialName("port") var port: Int? = null,
@SerialName("token") val token: String? = null, @SerialName("token") val token: String? = null,
@SerialName("tokens") val tokens: List<String>? = null,
@SerialName("heartbeat_interval") var heartbeatInterval: Long? = null, @SerialName("heartbeat_interval") var heartbeatInterval: Long? = null,
) )

View File

@ -76,6 +76,7 @@ internal object ShamrockConfig {
putBoolean("enable_self_msg", intent.getBooleanExtra("enable_self_msg", false)) // 推送自己发的消息 putBoolean("enable_self_msg", intent.getBooleanExtra("enable_self_msg", false)) // 推送自己发的消息
putBoolean("shell", intent.getBooleanExtra("shell", false)) // 开启Shell接口 putBoolean("shell", intent.getBooleanExtra("shell", false)) // 开启Shell接口
putBoolean("enable_sync_msg_as_sent_msg", intent.getBooleanExtra("enable_sync_msg_as_sent_msg", false)) // 推送同步消息 putBoolean("enable_sync_msg_as_sent_msg", intent.getBooleanExtra("enable_sync_msg_as_sent_msg", false)) // 推送同步消息
putBoolean("forbid_useless_process", intent.getBooleanExtra("forbid_useless_process", false)) // 禁用QQ生成无用进程
} }
Config.defaultToken = intent.getStringExtra("token") Config.defaultToken = intent.getStringExtra("token")
Config.antiTrace = intent.getBooleanExtra("anti_qq_trace", true) Config.antiTrace = intent.getBooleanExtra("anti_qq_trace", true)
@ -126,6 +127,10 @@ internal object ShamrockConfig {
return mmkv.getBoolean("enable_self_msg", false) return mmkv.getBoolean("enable_self_msg", false)
} }
fun forbidUselessProcess(): Boolean {
return mmkv.getBoolean("forbid_useless_process", false)
}
fun openWebSocketClient(): Boolean { fun openWebSocketClient(): Boolean {
return mmkv.getBoolean("ws_client", false) return mmkv.getBoolean("ws_client", false)
} }

View File

@ -6,5 +6,12 @@ import kotlinx.serialization.Serializable
@Serializable @Serializable
internal data class Credentials( internal data class Credentials(
@SerialName("token") val bkn: String = "", @SerialName("token") val bkn: String = "",
@SerialName("cookies") val cookie: String = "" @SerialName("cookies") val cookie: String = "",
@SerialName("bigdata_ticket") val bigDataTicket: BigDataTicket? = null
)
@Serializable
data class BigDataTicket(
var key: String? = null,
var sig: String? = null
) )

View File

@ -52,8 +52,10 @@ internal data class MessageEvent (
@SerialName("message_type") val messageType: MsgType, @SerialName("message_type") val messageType: MsgType,
@SerialName("sub_type") val subType: MsgSubType, @SerialName("sub_type") val subType: MsgSubType,
@SerialName("message_id") val messageId: Int, @SerialName("message_id") val messageId: Int,
@SerialName("group_id") val groupId: Long = 0, @SerialName("group_id") val groupId: Long = Long.MIN_VALUE,
@SerialName("target_id") val targetId: Long = 0, @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("peer_id") val peerId: Long,
@SerialName("user_id") val userId: Long, @SerialName("user_id") val userId: Long,
@SerialName("anonymous") val anonymous: Anonymous? = null, @SerialName("anonymous") val anonymous: Anonymous? = null,
@ -61,7 +63,8 @@ internal data class MessageEvent (
@SerialName("raw_message") val rawMessage: String, @SerialName("raw_message") val rawMessage: String,
@SerialName("font") val font: Int, @SerialName("font") val font: Int,
@SerialName("sender") val sender: Sender, @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) { enum class MessageTempSource(val id: Int) {
@ -86,7 +89,9 @@ internal data class Anonymous(
internal enum class MemberRole { internal enum class MemberRole {
@SerialName("owner") Owner, @SerialName("owner") Owner,
@SerialName("admin") Admin, @SerialName("admin") Admin,
@SerialName("member") Member @SerialName("member") Member,
@SerialName("stranger") Stranger,
@SerialName("unknown") Unknown
} }
@Serializable @Serializable

View File

@ -7,6 +7,7 @@ import com.tencent.qqnt.kernel.nativeinterface.*
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.qqinterface.servlet.TicketSvc import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import moe.fuqiuluo.qqinterface.servlet.msg.convert.toCQCode import moe.fuqiuluo.qqinterface.servlet.msg.convert.toCQCode
import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
@ -44,11 +45,11 @@ internal object AioListener : IKernelMsgListener {
it.value(record) it.value(record)
messageLessListenerMap.remove(it.key) messageLessListenerMap.remove(it.key)
} }
if (record.msgSeq < 0) return if (record.msgSeq < 0) return
val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId) val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId)
val peerId = when(record.chatType) { val peerId = when (record.chatType) {
MsgConstant.KCHATTYPEGUILD -> record.guildId MsgConstant.KCHATTYPEGUILD -> record.guildId
else -> record.peerUin.toString() else -> record.peerUin.toString()
} }
@ -87,12 +88,14 @@ internal object AioListener : IKernelMsgListener {
if (!rule.white.isNullOrEmpty() && !rule.white.contains(record.senderUin)) return if (!rule.white.isNullOrEmpty() && !rule.white.contains(record.senderUin)) return
} }
if(!GlobalEventTransmitter.MessageTransmitter.transGroupMessage( if (!GlobalEventTransmitter.MessageTransmitter.transGroupMessage(
record, record.elements, rawMsg, msgHash, postType record, record.elements, rawMsg, msgHash, postType
)) { )
) {
LogCenter.log("群消息推送失败 -> 推送目标可能不存在", Level.WARN) LogCenter.log("群消息推送失败 -> 推送目标可能不存在", Level.WARN)
} }
} }
MsgConstant.KCHATTYPEC2C -> { MsgConstant.KCHATTYPEC2C -> {
LogCenter.log("私聊消息(private = ${record.senderUin}, id = [$msgHash | ${record.msgId} | ${record.msgSeq}], msg = $rawMsg)") LogCenter.log("私聊消息(private = ${record.senderUin}, id = [$msgHash | ${record.msgId} | ${record.msgSeq}], msg = $rawMsg)")
ShamrockConfig.getPrivateRule()?.let { rule -> ShamrockConfig.getPrivateRule()?.let { rule ->
@ -100,9 +103,10 @@ internal object AioListener : IKernelMsgListener {
if (!rule.white.isNullOrEmpty() && !rule.white.contains(record.senderUin)) return if (!rule.white.isNullOrEmpty() && !rule.white.contains(record.senderUin)) return
} }
if(!GlobalEventTransmitter.MessageTransmitter.transPrivateMessage( if (!GlobalEventTransmitter.MessageTransmitter.transPrivateMessage(
record, record.elements, rawMsg, msgHash, postType record, record.elements, rawMsg, msgHash, postType
)) { )
) {
LogCenter.log("私聊消息推送失败 -> MessageTransmitter", Level.WARN) LogCenter.log("私聊消息推送失败 -> MessageTransmitter", Level.WARN)
} }
} }
@ -110,24 +114,40 @@ internal object AioListener : IKernelMsgListener {
MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> { MsgConstant.KCHATTYPETEMPC2CFROMGROUP -> {
if (!ShamrockConfig.allowTempSession()) return if (!ShamrockConfig.allowTempSession()) return
LogCenter.log("私聊临时消息(private = ${record.senderUin}, id = $msgHash, msg = $rawMsg)")
ShamrockConfig.getPrivateRule()?.let { rule -> ShamrockConfig.getPrivateRule()?.let { rule ->
if (!rule.black.isNullOrEmpty() && rule.black.contains(record.senderUin)) return if (!rule.black.isNullOrEmpty() && rule.black.contains(record.senderUin)) return
if (!rule.white.isNullOrEmpty() && !rule.white.contains(record.senderUin)) return if (!rule.white.isNullOrEmpty() && !rule.white.contains(record.senderUin)) return
} }
if(!GlobalEventTransmitter.MessageTransmitter.transPrivateMessage( var groupCode = 0L
record, record.elements, rawMsg, msgHash, tempSource = MessageTempSource.Group, postType = postType 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) LogCenter.log("私聊临时消息推送失败 -> MessageTransmitter", Level.WARN)
} }
} }
MsgConstant.KCHATTYPEGUILD -> { MsgConstant.KCHATTYPEGUILD -> {
LogCenter.log("频道消息(guildId = ${record.guildId}, sender=${record.senderUid}, id = [$msgHash | ${record.msgId}], msg = $rawMsg)") LogCenter.log("频道消息(guildId = ${record.guildId}, sender = ${record.senderUid}, id = [$msgHash | ${record.msgId}], msg = $rawMsg)")
if(!GlobalEventTransmitter.MessageTransmitter if (!GlobalEventTransmitter.MessageTransmitter
.transGuildMessage(record, record.elements, rawMsg, msgHash, postType = postType) .transGuildMessage(record, record.elements, rawMsg, msgHash, postType = postType)
) { ) {
LogCenter.log("频道消息推送失败 -> MessageTransmitter", Level.WARN) LogCenter.log("频道消息推送失败 -> MessageTransmitter", Level.WARN)
} }
} }
@ -149,8 +169,7 @@ internal object AioListener : IKernelMsgListener {
GlobalScope.launch { GlobalScope.launch {
try { try {
val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId) val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId)
val peerId = when (record.chatType) {
val peerId = when(record.chatType) {
MsgConstant.KCHATTYPEGUILD -> record.guildId MsgConstant.KCHATTYPEGUILD -> record.guildId
else -> record.peerUin.toString() else -> record.peerUin.toString()
} }
@ -174,8 +193,6 @@ internal object AioListener : IKernelMsgListener {
override fun onMsgInfoListUpdate(msgList: ArrayList<MsgRecord>?) { override fun onMsgInfoListUpdate(msgList: ArrayList<MsgRecord>?) {
msgList?.forEach { record -> msgList?.forEach { record ->
if (record.chatType == MsgConstant.KCHATTYPEGUILD) return@forEach// TODO: 频道消息暂不处理
if (record.sendStatus == MsgConstant.KSENDSTATUSFAILED if (record.sendStatus == MsgConstant.KSENDSTATUSFAILED
|| record.sendStatus == MsgConstant.KSENDSTATUSSENDING || record.sendStatus == MsgConstant.KSENDSTATUSSENDING
) { ) {
@ -184,7 +201,7 @@ internal object AioListener : IKernelMsgListener {
GlobalScope.launch { GlobalScope.launch {
val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId) val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId)
val peerId = when(record.chatType) { val peerId = when (record.chatType) {
MsgConstant.KCHATTYPEGUILD -> record.guildId MsgConstant.KCHATTYPEGUILD -> record.guildId
else -> record.peerUin.toString() else -> record.peerUin.toString()
} }
@ -449,6 +466,7 @@ internal object AioListener : IKernelMsgListener {
override fun onGuildMsgAbFlagChanged(guildMsgAbFlag: GuildMsgAbFlag?) { override fun onGuildMsgAbFlagChanged(guildMsgAbFlag: GuildMsgAbFlag?) {
} }
override fun onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: GuildNotificationAbstractInfo?) { override fun onGuildNotificationAbstractUpdate(guildNotificationAbstractInfo: GuildNotificationAbstractInfo?) {
} }

View File

@ -1,5 +1,4 @@
@file:OptIn(DelicateCoroutinesApi::class, ExperimentalSerializationApi::class) @file:OptIn(DelicateCoroutinesApi::class, ExperimentalSerializationApi::class)
package moe.fuqiuluo.shamrock.remote.service.listener package moe.fuqiuluo.shamrock.remote.service.listener
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant 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
import moe.fuqiuluo.qqinterface.servlet.GroupSvc.requestGroupSystemMsgNew import moe.fuqiuluo.qqinterface.servlet.GroupSvc.requestGroupSystemMsgNew
import moe.fuqiuluo.qqinterface.servlet.TicketSvc.getLongUin import moe.fuqiuluo.qqinterface.servlet.TicketSvc.getLongUin
import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
import moe.fuqiuluo.shamrock.helper.MessageHelper import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.remote.service.data.push.NoticeSubType import moe.fuqiuluo.shamrock.remote.service.data.push.NoticeSubType
import moe.fuqiuluo.shamrock.remote.service.data.push.NoticeType 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.asString
import moe.fuqiuluo.shamrock.tools.readBuf32Long import moe.fuqiuluo.shamrock.tools.readBuf32Long
import moe.fuqiuluo.shamrock.xposed.helper.PacketHandler import moe.fuqiuluo.shamrock.xposed.helper.PacketHandler
import protobuf.message.MessageContentHead import protobuf.message.MessageContent
import protobuf.message.MessageHead import protobuf.message.MessageHead
import protobuf.message.RichMessage import protobuf.message.MessageBody
import protobuf.message.multimedia.RichMediaForPicData
import protobuf.push.C2CCommonTipsEvent import protobuf.push.C2CCommonTipsEvent
import protobuf.push.C2CRecallEvent import protobuf.push.C2CRecallEvent
import protobuf.push.FriendApplyEvent import protobuf.push.FriendApplyEvent
@ -46,6 +47,9 @@ import protobuf.push.GroupInvitedApplyEvent
import protobuf.push.GroupListChangeEvent import protobuf.push.GroupListChangeEvent
import protobuf.push.MessagePush import protobuf.push.MessagePush
import protobuf.push.MessagePushClientInfo import protobuf.push.MessagePushClientInfo
import java.util.regex.Pattern
private val RKEY_PATTERN = Pattern.compile("rkey=([A-Za-z0-9_-]+)")
internal object PrimitiveListener { internal object PrimitiveListener {
fun registerListener() { fun registerListener() {
@ -65,7 +69,7 @@ internal object PrimitiveListener {
if ( if (
push.msgBody == null || push.msgBody == null ||
push.msgBody!!.contentHead == null || push.msgBody!!.contentHead == null ||
push.msgBody!!.richMsg == null || push.msgBody!!.body == null ||
push.msgBody!!.contentHead!!.msgTime == null push.msgBody!!.contentHead!!.msgTime == null
) return ) return
val msgBody = push.msgBody!! val msgBody = push.msgBody!!
@ -73,29 +77,30 @@ internal object PrimitiveListener {
val msgType = contentHead.msgType val msgType = contentHead.msgType
val subType = contentHead.msgSubType val subType = contentHead.msgSubType
val msgTime = contentHead.msgTime!! val msgTime = contentHead.msgTime!!
val richMsg = msgBody.richMsg!! val body = msgBody.body!!
try { try {
when (msgType) { when (msgType) {
33 -> onGroupMemIncreased(msgTime, richMsg) 33 -> onGroupMemIncreased(msgTime, body)
34 -> onGroupMemberDecreased(msgTime, richMsg) 34 -> onGroupMemberDecreased(msgTime, body)
44 -> onGroupAdminChange(msgTime, richMsg) 44 -> onGroupAdminChange(msgTime, body)
84 -> onGroupApply(msgTime, contentHead, richMsg) 82 -> onGroupMessage(msgTime, body)
87 -> onInviteGroup(msgTime, msgBody.msgHead!!, richMsg) 84 -> onGroupApply(msgTime, contentHead, body)
87 -> onInviteGroup(msgTime, msgBody.msgHead!!, body)
528 -> when (subType) { 528 -> when (subType) {
35 -> onFriendApply(msgTime, push.clientInfo!!, richMsg) 35 -> onFriendApply(msgTime, push.clientInfo!!, body)
39 -> onCardChange(msgTime, richMsg) 39 -> onCardChange(msgTime, body)
// invite // invite
68 -> onGroupApply(msgTime, contentHead, richMsg) 68 -> onGroupApply(msgTime, contentHead, body)
138 -> onC2CRecall(msgTime, richMsg) 138 -> onC2CRecall(msgTime, body)
290 -> onC2CPoke(msgTime, richMsg) 290 -> onC2CPoke(msgTime, body)
} }
732 -> when (subType) { 732 -> when (subType) {
12 -> onGroupBan(msgTime, richMsg) 12 -> onGroupBan(msgTime, body)
16 -> onGroupUniqueTitleChange(msgTime, richMsg) 16 -> onGroupUniqueTitleChange(msgTime, body)
17 -> onGroupRecall(msgTime, richMsg) 17 -> onGroupRecall(msgTime, body)
20 -> onGroupPokeAndGroupSign(msgTime, richMsg) 20 -> onGroupPokeAndGroupSign(msgTime, body)
21 -> onEssenceMessage(msgTime, push.clientInfo, richMsg) 21 -> onEssenceMessage(msgTime, push.clientInfo, body)
} }
} }
} catch (e: Exception) { } 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!!) val event = ProtoBuf.decodeFromByteArray<C2CCommonTipsEvent>(richMsg.rawBuffer!!)
if (event.params == null) return if (event.params == null) return
@ -129,7 +154,7 @@ internal object PrimitiveListener {
private suspend fun onFriendApply( private suspend fun onFriendApply(
msgTime: Long, msgTime: Long,
clientInfo: MessagePushClientInfo, clientInfo: MessagePushClientInfo,
richMsg: RichMessage richMsg: MessageBody
) { ) {
val event = ProtoBuf.decodeFromByteArray<FriendApplyEvent>(richMsg.rawBuffer!!) val event = ProtoBuf.decodeFromByteArray<FriendApplyEvent>(richMsg.rawBuffer!!)
if (event.head == null) return 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) LogCenter.log("群名片事件异常请尝试提交issue", Level.WARN)
/*try { /*try {
val readPacket = ByteReadPacket(richMsg.rawBuffer!!) 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 { val event = runCatching {
ProtoBuf.decodeFromByteArray<GroupCommonTipsEvent>(richMsg.rawBuffer!!) ProtoBuf.decodeFromByteArray<GroupCommonTipsEvent>(richMsg.rawBuffer!!)
}.getOrElse { }.getOrElse {
@ -250,7 +275,7 @@ internal object PrimitiveListener {
private suspend fun onEssenceMessage( private suspend fun onEssenceMessage(
msgTime: Long, msgTime: Long,
clientInfo: MessagePushClientInfo?, clientInfo: MessagePushClientInfo?,
richMsg: RichMessage richMsg: MessageBody
) { ) {
if (clientInfo == null) return if (clientInfo == null) return
val event = runCatching { 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 { val event = runCatching {
ProtoBuf.decodeFromByteArray<GroupCommonTipsEvent>(richMsg.rawBuffer!!) ProtoBuf.decodeFromByteArray<GroupCommonTipsEvent>(richMsg.rawBuffer!!)
}.getOrElse { }.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 event = ProtoBuf.decodeFromByteArray<C2CRecallEvent>(richMsg.rawBuffer!!)
val head = event.head!! 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 event = ProtoBuf.decodeFromByteArray<GroupListChangeEvent>(richMsg.rawBuffer!!)
val groupCode = event.groupCode val groupCode = event.groupCode
val targetUid = event.memberUid 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 event = ProtoBuf.decodeFromByteArray<GroupListChangeEvent>(richMsg.rawBuffer!!)
val groupCode = event.groupCode val groupCode = event.groupCode
val targetUid = event.memberUid 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 event = ProtoBuf.decodeFromByteArray<GroupAdminChangeEvent>(richMsg.rawBuffer!!)
val groupCode = event.groupCode val groupCode = event.groupCode
if (event.operation == null) return 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 event = ProtoBuf.decodeFromByteArray<GroupBanEvent>(richMsg.rawBuffer!!)
val groupCode = event.groupCode.toLong() val groupCode = event.groupCode.toLong()
val operatorUid = event.operatorUid 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 { val event = runCatching {
ProtoBuf.decodeFromByteArray<GroupCommonTipsEvent>(richMsg.rawBuffer!!) ProtoBuf.decodeFromByteArray<GroupCommonTipsEvent>(richMsg.rawBuffer!!)
}.getOrElse { }.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) { when (contentHead.msgType) {
84 -> { 84 -> {
val event = ProtoBuf.decodeFromByteArray<GroupApplyEvent>(richMsg.rawBuffer!!) val event = ProtoBuf.decodeFromByteArray<GroupApplyEvent>(richMsg.rawBuffer!!)
val groupCode = event.groupCode val groupCode = event.groupCode
val applierUid = event.applierUid val applierUid = event.applierUid
val reason = event.applyMsg ?: "" val reason = event.applyMsg ?: ""
val applier = ContactHelper.getUinByUidAsync(applierUid).toLong() var applier = ContactHelper.getUinByUidAsync(applierUid).toLong()
if (applier == getLongUin()) { if (applier == getLongUin()) {
return return
} }
val msgSeq = contentHead.msgSeq val msgSeq = contentHead.msgSeq
LogCenter.log("入群申请($groupCode) $applier: \"$reason\", seq: $msgSeq")
val flag = try { val flag = try {
var reqs = requestGroupSystemMsgNew(10, 1) var reqs = requestGroupSystemMsgNew(10, 1)
val riskReqs = requestGroupSystemMsgNew(5, 2) val riskReqs = requestGroupSystemMsgNew(5, 2)
@ -548,10 +572,14 @@ internal object PrimitiveListener {
it.msg_time.get() == time && it.msg?.group_code?.get() == groupCode it.msg_time.get() == time && it.msg?.group_code?.get() == groupCode
} }
val seq = req?.msg_seq?.get() ?: time val seq = req?.msg_seq?.get() ?: time
if (applier == 0L) {
applier = req?.req_uin?.get() ?: 0L
}
"$seq;$groupCode;$applier" "$seq;$groupCode;$applier"
} catch (err: Throwable) { } catch (err: Throwable) {
"$time;$groupCode;$applier" "$time;$groupCode;$applier"
} }
LogCenter.log("入群申请($groupCode) $applier: \"$reason\", seq: $msgSeq")
if (!GlobalEventTransmitter.RequestTransmitter if (!GlobalEventTransmitter.RequestTransmitter
.transGroupApply(time, applier, applierUid, reason, groupCode, flag, RequestSubType.Add) .transGroupApply(time, applier, applierUid, reason, groupCode, flag, RequestSubType.Add)
) { ) {
@ -562,7 +590,7 @@ internal object PrimitiveListener {
val event = ProtoBuf.decodeFromByteArray<GroupInvitedApplyEvent>(richMsg.rawBuffer!!) val event = ProtoBuf.decodeFromByteArray<GroupInvitedApplyEvent>(richMsg.rawBuffer!!)
val groupCode = event.applyInfo?.groupCode ?: return val groupCode = event.applyInfo?.groupCode ?: return
val applierUid = event.applyInfo?.applierUid ?: return val applierUid = event.applyInfo?.applierUid ?: return
val applier = ContactHelper.getUinByUidAsync(applierUid).toLong() var applier = ContactHelper.getUinByUidAsync(applierUid).toLong()
if (applier == getLongUin()) { if (applier == getLongUin()) {
return return
} }
@ -570,7 +598,6 @@ internal object PrimitiveListener {
// todo // todo
return return
} }
LogCenter.log("邀请入群申请($groupCode): $applier")
val flag = try { val flag = try {
var reqs = requestGroupSystemMsgNew(10, 1) var reqs = requestGroupSystemMsgNew(10, 1)
val riskReqs = requestGroupSystemMsgNew(5, 2) val riskReqs = requestGroupSystemMsgNew(5, 2)
@ -579,10 +606,14 @@ internal object PrimitiveListener {
it.msg_time.get() == time it.msg_time.get() == time
} }
val seq = req?.msg_seq?.get() ?: time val seq = req?.msg_seq?.get() ?: time
if (applier == 0L) {
applier = req?.req_uin?.get() ?: 0L
}
"$seq;$groupCode;$applier" "$seq;$groupCode;$applier"
} catch (err: Throwable) { } catch (err: Throwable) {
"$time;$groupCode;$applier" "$time;$groupCode;$applier"
} }
LogCenter.log("邀请入群申请($groupCode): $applier")
if (!GlobalEventTransmitter.RequestTransmitter if (!GlobalEventTransmitter.RequestTransmitter
.transGroupApply(time, applier, applierUid, "", groupCode, flag, RequestSubType.Add) .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 event = ProtoBuf.decodeFromByteArray<GroupInviteEvent>(richMsg.rawBuffer!!)
val groupCode = event.groupCode val groupCode = event.groupCode
val invitorUid = event.inviterUid val invitorUid = event.inviterUid

View File

@ -65,16 +65,14 @@ val Map<String, Any>.json: JsonObject
get() { get() {
val map = hashMapOf<String, JsonElement>() val map = hashMapOf<String, JsonElement>()
forEach { (key, any) -> forEach { (key, any) ->
if (any != null) { when (any) {
when (any) { is JsonElement -> map[key] = any
is JsonElement -> map[key] = any is Number -> map[key] = any.json
is Number -> map[key] = any.json is String -> map[key] = any.json
is String -> map[key] = any.json is Boolean -> map[key] = any.json
is Boolean -> map[key] = any.json is Map<*, *> -> map[key] = (any as Map<String, Any>).json
is Map<*, *> -> map[key] = (any as Map<String, Any>).json is Collection<*> -> map[key] = (any as Collection<Any>).json
is Collection<*> -> map[key] = (any as Collection<Any>).json else -> error("unknown object type: ${any::class.java}")
else -> error("unknown object type: ${any::class.java}")
}
} }
} }
return map.jsonObject return map.jsonObject

View File

@ -1,10 +1,14 @@
package moe.fuqiuluo.shamrock.xposed package moe.fuqiuluo.shamrock.xposed
import android.content.Context import android.content.Context
import android.os.Process
import de.robv.android.xposed.IXposedHookLoadPackage import de.robv.android.xposed.IXposedHookLoadPackage
import de.robv.android.xposed.XposedBridge import de.robv.android.xposed.XposedBridge
import de.robv.android.xposed.callbacks.XC_LoadPackage import de.robv.android.xposed.callbacks.XC_LoadPackage
import de.robv.android.xposed.XposedBridge.log 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.utils.MMKVFetcher
import moe.fuqiuluo.shamrock.xposed.loader.KeepAlive import moe.fuqiuluo.shamrock.xposed.loader.KeepAlive
import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader import moe.fuqiuluo.shamrock.xposed.loader.LuoClassloader
@ -15,12 +19,14 @@ import moe.fuqiuluo.shamrock.xposed.hooks.runFirstActions
import mqq.app.MobileQQ import mqq.app.MobileQQ
import java.lang.reflect.Field import java.lang.reflect.Field
import java.lang.reflect.Modifier import java.lang.reflect.Modifier
import kotlin.system.exitProcess
internal const val PACKAGE_NAME_QQ = "com.tencent.mobileqq" private const val PACKAGE_NAME_QQ = "com.tencent.mobileqq"
internal const val PACKAGE_NAME_QQ_INTERNATIONAL = "com.tencent.mobileqqi" private const val PACKAGE_NAME_QQ_INTERNATIONAL = "com.tencent.mobileqqi"
internal const val PACKAGE_NAME_QQ_LITE = "com.tencent.qqlite" private const val PACKAGE_NAME_QQ_LITE = "com.tencent.qqlite"
internal const val PACKAGE_NAME_TIM = "com.tencent.tim" private const val PACKAGE_NAME_TIM = "com.tencent.tim"
private val uselessProcess = listOf("peak", "tool", "mini", "qzone")
internal class XposedEntry: IXposedHookLoadPackage { internal class XposedEntry: IXposedHookLoadPackage {
companion object { companion object {
@ -121,9 +127,7 @@ internal class XposedEntry: IXposedHookLoadPackage {
System.setProperty("qxbot_flag", "1") System.setProperty("qxbot_flag", "1")
} else return } else return
log("Process Name = " + MobileQQ.getMobileQQ().qqProcessName) val processName = MobileQQ.getMobileQQ().qqProcessName
PlatformUtils.isTim()
// MSG LISTENER 进程运行在主进程 // MSG LISTENER 进程运行在主进程
// API 也应该开放在主进程 // API 也应该开放在主进程
@ -134,6 +138,22 @@ internal class XposedEntry: IXposedHookLoadPackage {
MMKVFetcher.initMMKV(ctx) 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) runFirstActions(ctx)
} }
} }

View File

@ -2,24 +2,17 @@ package moe.fuqiuluo.shamrock.xposed.helper
import com.tencent.qqnt.kernel.api.IKernelService import com.tencent.qqnt.kernel.api.IKernelService
import com.tencent.qqnt.kernel.api.impl.MsgService 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.IOperateCallback
import com.tencent.qqnt.kernel.nativeinterface.IQQNTWrapperSession 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.Mutex
import kotlinx.coroutines.sync.withLock import kotlinx.coroutines.sync.withLock
import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.remote.service.PacketReceiver import moe.fuqiuluo.shamrock.remote.service.PacketReceiver
import moe.fuqiuluo.shamrock.remote.service.listener.AioListener 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.remote.service.listener.PrimitiveListener
import moe.fuqiuluo.shamrock.tools.hookMethod import moe.fuqiuluo.shamrock.tools.hookMethod
import moe.fuqiuluo.shamrock.utils.PlatformUtils import moe.fuqiuluo.shamrock.utils.PlatformUtils
import kotlin.reflect.jvm.javaMethod
internal object NTServiceFetcher { internal object NTServiceFetcher {
private lateinit var iKernelService: IKernelService private lateinit var iKernelService: IKernelService
@ -30,7 +23,7 @@ internal object NTServiceFetcher {
lock.withLock { lock.withLock {
val msgService = service.msgService ?: return val msgService = service.msgService ?: return
val sessionService = service.wrapperSession ?: return val sessionService = service.wrapperSession ?: return
val groupService = sessionService.groupService ?: return //val groupService = sessionService.groupService ?: return
val curHash = service.hashCode() + msgService.hashCode() val curHash = service.hashCode() + msgService.hashCode()
if (isInitForNt(curHash)) return if (isInitForNt(curHash)) return
@ -43,7 +36,7 @@ internal object NTServiceFetcher {
this.iKernelService = service this.iKernelService = service
initNTKernelListener(msgService, groupService) initNTKernelListener(msgService)
antiBackgroundMode(sessionService) antiBackgroundMode(sessionService)
//hookGuildListener(sessionService) //hookGuildListener(sessionService)
} }
@ -66,7 +59,7 @@ internal object NTServiceFetcher {
return hash == curKernelHash return hash == curKernelHash
} }
private fun initNTKernelListener(msgService: MsgService, groupService: IKernelGroupService) { private fun initNTKernelListener(msgService: MsgService) {
if (!PlatformUtils.isMainProcess()) return if (!PlatformUtils.isMainProcess()) return
try { try {

View File

@ -1,12 +1,50 @@
@file:Suppress("UNUSED_VARIABLE", "LocalVariableName")
package moe.fuqiuluo.shamrock.xposed.hooks package moe.fuqiuluo.shamrock.xposed.hooks
import android.content.Context 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 moe.fuqiuluo.symbols.XposedHook
import java.lang.reflect.Modifier
@XposedHook(priority = -1) @XposedHook(priority = -1, process = Process.ALL)
internal class HookForDebug: IAction { internal class HookForDebug: IAction {
override fun invoke(ctx: Context) { 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 val httpEngineService = AppRuntimeFetcher.appRuntime
.getRuntimeService(IHttpEngineService::class.java, "all") .getRuntimeService(IHttpEngineService::class.java, "all")
httpEngineService.javaClass.hookMethod("sendReq").before { httpEngineService.javaClass.hookMethod("sendReq").before {
@ -19,7 +57,17 @@ internal class HookForDebug: IAction {
LogCenter.log("请求地址: ${req.mReqUrl}") LogCenter.log("请求地址: ${req.mReqUrl}")
LogCenter.log("请求: ${req.toInnerValuesString(NetReq::class.java)}") 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())
}
}*/ }*/
}
}

View File

@ -38,7 +38,7 @@ internal class InitRemoteService : IAction {
if (ShamrockConfig.allowWebHook()) { if (ShamrockConfig.allowWebHook()) {
HttpService.initTransmitter() HttpService.init()
} }
val runtime = AppRuntimeFetcher.appRuntime val runtime = AppRuntimeFetcher.appRuntime