Shamrock: Support du chat de groupe et transfert de messages

This commit is contained in:
WhiteChi 2023-11-10 00:34:51 +08:00
parent d28c6dc820
commit bc967cf926
17 changed files with 510 additions and 175 deletions

View File

@ -173,24 +173,19 @@ dependencies {
implementation("io.coil-kt:coil:2.4.0") implementation("io.coil-kt:coil:2.4.0")
implementation("io.coil-kt:coil-compose:2.4.0") implementation("io.coil-kt:coil-compose:2.4.0")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")
implementation("org.jetbrains.kotlinx:kotlinx-io-jvm:0.1.16") implementation("org.jetbrains.kotlinx:kotlinx-io-jvm:0.1.16")
implementation("io.ktor:ktor-server-core:2.3.3")
implementation("io.ktor:ktor-server-host-common:2.3.3") val ktorVersion = "2.3.3"
implementation("io.ktor:ktor-server-status-pages:2.3.3") implementation("io.ktor:ktor-server-core:$ktorVersion")
implementation("io.ktor:ktor-server-netty:2.3.3") implementation("io.ktor:ktor-server-host-common:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.3") implementation("io.ktor:ktor-server-status-pages:$ktorVersion")
implementation("io.ktor:ktor-server-content-negotiation:2.3.3") implementation("io.ktor:ktor-server-netty:$ktorVersion")
implementation("io.ktor:ktor-client-core:2.3.3") implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-client-cio:2.3.3") implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-content-negotiation:2.3.3") implementation("io.ktor:ktor-client-cio:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.3") implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
// useless implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
//implementation ("com.maxkeppeler.sheets-compose-dialogs:core:1.2.0") //implementation("io.ktor:ktor-serialization-kotlinx-protobuf:$ktorVersion")
//implementation ("com.maxkeppeler.sheets-compose-dialogs:info:1.2.0")
//implementation ("com.maxkeppeler.sheets-compose-dialogs:input:1.2.0")
//implementation ("com.maxkeppeler.sheets-compose-dialogs:list:1.2.0")
//implementation ("com.maxkeppeler.sheets-compose-dialogs:state:1.2.0")
implementation(project(":xposed")) implementation(project(":xposed"))

View File

@ -1,6 +1,50 @@
package com.tencent.mobileqq.pb; package com.tencent.mobileqq.pb;
import java.lang.reflect.Field;
import java.util.Arrays;
public class MessageMicro<T extends MessageMicro<T>> { public class MessageMicro<T extends MessageMicro<T>> {
public static final class FieldMap {
private Object[] defaultValues;
private Field[] fields;
private int[] tags;
FieldMap(int[] iArr, String[] strArr, Object[] objArr, Class<?> cls) {
this.tags = iArr;
this.defaultValues = objArr;
this.fields = new Field[iArr.length];
for (int i2 = 0; i2 < iArr.length; i2++) {
try {
this.fields[i2] = cls.getField(strArr[i2]);
} catch (Exception e2) {
e2.printStackTrace();
}
}
}
void clear(MessageMicro<?> messageMicro) {
}
<U extends MessageMicro<U>> void copyFields(U u, U u2) {
}
Field get(int i2) {
int binarySearch = Arrays.binarySearch(this.tags, i2);
if (binarySearch < 0) {
return null;
}
return this.fields[binarySearch];
}
int getSerializedSize(MessageMicro<?> messageMicro) {
return 0;
}
}
public static FieldMap initFieldMap(int[] iArr, String[] strArr, Object[] objArr, Class<?> cls) {
return new FieldMap(iArr, strArr, objArr, cls);
}
public final T mergeFrom(byte[] bArr) { public final T mergeFrom(byte[] bArr) {
return null; return null;
} }

View File

@ -30,6 +30,10 @@ public interface IKernelMsgService {
void getMultiMsg(Contact contact, long rootMsgId, long parentMsgId, IGetMultiMsgCallback cb); void getMultiMsg(Contact contact, long rootMsgId, long parentMsgId, IGetMultiMsgCallback cb);
void multiForwardMsg(ArrayList<MultiMsgInfo> arrayList, Contact from, Contact to, IOperateCallback cb);
void setAllC2CAndGroupMsgRead(IOperateCallback cb);
void clearMsgRecords(Contact contact, IClearMsgRecordsCallback cb); void clearMsgRecords(Contact contact, IClearMsgRecordsCallback cb);
String createUidFromTinyId(long j2, long j3); String createUidFromTinyId(long j2, long j3);

View File

@ -1,7 +1,5 @@
package com.tencent.qqnt.kernel.nativeinterface; package com.tencent.qqnt.kernel.nativeinterface;
/* compiled from: P */
/* loaded from: classes2.dex */
public final class MultiMsgInfo { public final class MultiMsgInfo {
long msgId; long msgId;
String senderShowName; String senderShowName;
@ -21,8 +19,6 @@ public final class MultiMsgInfo {
return "MultiMsgInfo{msgId=" + this.msgId + ",senderShowName=" + this.senderShowName + ",}"; return "MultiMsgInfo{msgId=" + this.msgId + ",senderShowName=" + this.senderShowName + ",}";
} }
public MultiMsgInfo(long j2, String str) { public MultiMsgInfo(long msgId, String showName) {
this.msgId = j2;
this.senderShowName = str;
} }
} }

View File

@ -74,6 +74,7 @@ dependencies {
implementation("io.ktor:ktor-client-cio:$ktorVersion") implementation("io.ktor:ktor-client-cio:$ktorVersion")
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
//implementation("io.ktor:ktor-serialization-kotlinx-protobuf:$ktorVersion")
implementation("io.ktor:ktor-network-tls-certificates:$ktorVersion") implementation("io.ktor:ktor-network-tls-certificates:$ktorVersion")
/** /**

View File

@ -0,0 +1,5 @@
@file:OptIn(ExperimentalSerializationApi::class)
package moe.fuqiuluo.qqinterface.entries
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable

View File

@ -101,7 +101,7 @@ internal abstract class BaseSvc {
app.sendToService(to) app.sendToService(to)
} }
fun sendBuffer(cmd: String, isPb: Boolean, buffer: ByteArray, seq: Int) { fun sendBuffer(cmd: String, isPb: Boolean, buffer: ByteArray, seq: Int = MsfCore.getNextSeq()) {
val toServiceMsg = ToServiceMsg("mobileqq.service", app.currentUin, cmd) val toServiceMsg = ToServiceMsg("mobileqq.service", app.currentUin, cmd)
toServiceMsg.putWupBuffer(buffer) toServiceMsg.putWupBuffer(buffer)
toServiceMsg.addAttribute("req_pb_protocol_flag", isPb) toServiceMsg.addAttribute("req_pb_protocol_flag", isPb)

View File

@ -1,3 +1,5 @@
@file:OptIn(DelicateCoroutinesApi::class)
package moe.fuqiuluo.qqinterface.servlet package moe.fuqiuluo.qqinterface.servlet
import com.tencent.mobileqq.qroute.QRoute import com.tencent.mobileqq.qroute.QRoute
@ -9,9 +11,15 @@ import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
import com.tencent.qqnt.kernel.nativeinterface.TempChatGameSession import com.tencent.qqnt.kernel.nativeinterface.TempChatGameSession
import com.tencent.qqnt.kernel.nativeinterface.TempChatPrepareInfo 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.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.time.withTimeoutOrNull
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.json.JsonArray import kotlinx.serialization.json.JsonArray
import moe.fuqiuluo.proto.protobufOf
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
@ -19,6 +27,7 @@ import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
import moe.fuqiuluo.shamrock.xposed.helper.msgService import moe.fuqiuluo.shamrock.xposed.helper.msgService
import java.util.UUID
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine import kotlin.coroutines.suspendCoroutine
@ -183,6 +192,44 @@ internal object MsgSvc: BaseSvc() {
return MessageHelper.sendMessageWithoutMsgId(chatType, peedId, message, MessageCallback(peedId, 0), fromId) return MessageHelper.sendMessageWithoutMsgId(chatType, peedId, message, MessageCallback(peedId, 0), fromId)
} }
suspend fun getMultiMsg(resId: String): Result<List<MsgRecord>> {
val kernelService = NTServiceFetcher.kernelService
val sessionService = kernelService.wrapperSession
val msgService = sessionService.msgService
val contact = MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, TicketSvc.getUin())
val content = "{\"app\":\"com.tencent.multimsg\",\"config\":{\"autosize\":1,\"forward\":1,\"round\":1,\"type\":\"normal\",\"width\":300},\"desc\":\"[聊天记录]\",\"extra\":\"\",\"meta\":{\"detail\":{\"news\":[{\"text\":\"Shamrock: 这是条假消息!\"}],\"resid\":\"$resId\",\"source\":\"聊天记录\",\"summary\":\"转发消息\",\"uniseq\":\"${UUID.randomUUID()}\"}},\"prompt\":\"[聊天记录]\",\"ver\":\"0.0.0.5\",\"view\":\"contact\"}"
val msgId = PacketSvc.fakeSelfRecvJsonMsg(msgService, content)
if (msgId < 0) {
return Result.failure(Exception("获取合并转发消息ID失败"))
}
val msgList = withTimeoutOrNull(5000L) {
suspendCancellableCoroutine<ArrayList<MsgRecord>> {
val job = GlobalScope.launch {
var hasResult = false
while (!hasResult) {
msgService.getMultiMsg(contact, msgId, msgId) { code, why, msgList ->
if (code == 0) {
it.resume(msgList)
hasResult = true
} else {
LogCenter.log("获取合并转发消息失败: $code($why): $msgId", Level.ERROR)
}
}
delay(200)
}
}
it.invokeOnCancellation {
job.cancel()
}
}
} ?: return Result.failure(Exception("获取合并转发消息失败"))
//msgService.deleteMsg(contact, arrayListOf(msgId), null)
return Result.success(msgList)
}
class MessageCallback( class MessageCallback(
private val peerId: String, private val peerId: String,
var msgHash: Int var msgHash: Int

View File

@ -0,0 +1,98 @@
package moe.fuqiuluo.qqinterface.servlet
import com.tencent.mobileqq.msf.core.MsfCore
import com.tencent.qqnt.kernel.nativeinterface.Contact
import com.tencent.qqnt.kernel.nativeinterface.IKernelMsgService
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import io.ktor.utils.io.core.BytePacketBuilder
import io.ktor.utils.io.core.readBytes
import io.ktor.utils.io.core.writeFully
import io.ktor.utils.io.core.writeInt
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import moe.fuqiuluo.proto.protobufOf
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.remote.action.handlers.GetHistoryMsg
import moe.fuqiuluo.shamrock.remote.service.listener.AioListener
import moe.fuqiuluo.shamrock.tools.broadcast
import moe.fuqiuluo.shamrock.utils.DeflateTools
import mqq.app.MobileQQ
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
import kotlin.math.abs
import kotlin.random.Random
import kotlin.random.nextInt
import kotlin.random.nextLong
internal object PacketSvc: BaseSvc() {
/**
* 伪造收到Json卡片消息
*/
suspend fun fakeSelfRecvJsonMsg(msgService: IKernelMsgService, content: String): Long {
return fakeReceiveSelfMsg(msgService) { arrayOf(
mapOf(
51 to 1 to (byteArrayOf(1) + DeflateTools.compress(content.toByteArray()))
)
) }
}
private suspend fun fakeReceiveSelfMsg(msgService: IKernelMsgService, builder: () -> Array<Map<*, *>>): Long {
val latestMsg = withTimeoutOrNull(3000) {
suspendCancellableCoroutine {
msgService.getMsgs(Contact(MsgConstant.KCHATTYPEC2C, app.currentUid, ""), 0L, 1, true) { code, why, msgs ->
it.resume(GetHistoryMsg.GetMsgResult(code, why, msgs))
}
}
}?.data?.firstOrNull()
val msgSeq = (latestMsg?.msgSeq ?: 0) + 1
fakeReceive("trpc.msg.olpush.OlPushService.MsgPush", 10000, protobufOf(
1 to mapOf(
1 to mapOf(
1 to app.currentUin.toLong(),
2 to app.currentUid,
3 to 1001,
5 to app.currentUin.toLong(),
6 to app.currentUid
),
2 to mapOf(
1 to 166,
3 to 11,
4 to msgSeq,
5 to msgSeq,
6 to (System.currentTimeMillis() / 1000).toInt(),
7 to 1,
11 to msgSeq,
12 to msgService.getMsgUniqueId(System.currentTimeMillis()),
14 to msgSeq - 2,
28 to msgSeq
),
3 to 1 to 2 to builder()
)
).toByteArray())
return withTimeoutOrNull(5000L) {
suspendCancellableCoroutine {
AioListener.messageLessListenerMap[msgSeq] = {
it.resume(this.msgId)
}
}
} ?: -1L
}
/**
* 伪造QQ收到某个包
*/
private fun fakeReceive(cmd: String, seq: Int, buffer: ByteArray) {
MobileQQ.getContext().broadcast("msf") {
putExtra("__cmd", "fake_packet")
putExtra("package_cmd", cmd)
putExtra("package_uin", app.currentUin)
putExtra("package_seq", seq)
val wupBuffer = BytePacketBuilder().apply {
writeInt(buffer.size + 4)
writeFully(buffer)
}.build()
putExtra("package_buffer", wupBuffer.readBytes())
wupBuffer.release()
}
}
}

View File

@ -1,103 +0,0 @@
package moe.fuqiuluo.qqinterface.servlet.msg
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord
import moe.fuqiuluo.proto.ProtoUtils
import moe.fuqiuluo.proto.asUtf8String
import moe.fuqiuluo.proto.protobufOf
import moe.fuqiuluo.qqinterface.servlet.BaseSvc
import moe.fuqiuluo.shamrock.tools.slice
import moe.fuqiuluo.shamrock.utils.DeflateTools
internal object LongMsgHelper: BaseSvc() {
private const val GROUP_LONG_MSG_CMD = "trpc.group.long_msg_interface.MsgService.SsoSendLongMsg"
suspend fun uploadGroupMsg(groupId: String, msgs: List<MsgRecord>): String {
val reqBody = protobufOf(
2 to mapOf(
1 to 3,
2 to 2 to groupId,
3 to groupId.toLong(),
4 to DeflateTools.gzip(toGroupByteArray(msgs))
),
15 to mapOf(
1 to 4,
2 to 2,
3 to 9,
4 to 0
)
).toByteArray()
val buffer = sendBufferAW(GROUP_LONG_MSG_CMD, true, reqBody)
?: error("获取消息资源ID失败")
val pb = ProtoUtils.decodeFromByteArray(buffer.slice(4))
return pb[2, 3].asUtf8String
}
private fun toGroupByteArray(msgs: List<MsgRecord>): ByteArray {
return protobufOf(
2 to mapOf(
1 to "MultiMsg",
2 to 1 to msgs.map { record ->
mapOf(
1 to mapOf(
2 to record.senderUid,
8 to mapOf(
1 to record.peerUin,
4 to record.sendNickName,
5 to 2
)
),
2 to mapOf(
1 to 82,
4 to record.msgRandom,
5 to record.msgSeq,
6 to record.msgTime,
7 to 1,
8 to 0,
9 to 0,
15 to mapOf(
1 to 0,
2 to 0,
3 to 0,
4 to "",
5 to ""
)
),
3 to mapOf(
1 to 2 to (record.elements.map {
when (val type = it.elementType) {
MsgConstant.KELEMTYPETEXT -> mapOf(1 to 1 to it.textElement.content)
else -> error("不支持的合并转发消息类型: $type")
}
} as ArrayList<Any>).also {
it.add(0, mapOf(
37 to mapOf(
1 to 8,
16 to 0,
17 to 0,
19 to mapOf(
15 to 65536,
25 to 0,
30 to 0,
31 to 0,
34 to 0,
41 to 0,
52 to 64,
54 to 1,
55 to 0,
72 to 0,
73 to 1 to 0,
96 to 0
)
)
))
}
)
)
}
)
).toByteArray()
}
}

View File

@ -53,8 +53,8 @@ internal object MessageConvert {
MsgConstant.KELEMTYPEREPLY to MessageElemConverter.ReplyConverter, MsgConstant.KELEMTYPEREPLY to MessageElemConverter.ReplyConverter,
MsgConstant.KELEMTYPEGRAYTIP to MessageElemConverter.GrayTipsConverter, MsgConstant.KELEMTYPEGRAYTIP to MessageElemConverter.GrayTipsConverter,
MsgConstant.KELEMTYPEFILE to MessageElemConverter.FileConverter, MsgConstant.KELEMTYPEFILE to MessageElemConverter.FileConverter,
MsgConstant.KELEMTYPEMULTIFORWARD to MessageElemConverter.XmlMultiMsgConverter, //MsgConstant.KELEMTYPEMULTIFORWARD to MessageElemConverter.XmlMultiMsgConverter,
MsgConstant.KELEMTYPESTRUCTLONGMSG to MessageElemConverter.XmlLongMsgConverter, //MsgConstant.KELEMTYPESTRUCTLONGMSG to MessageElemConverter.XmlLongMsgConverter,
) )
} }

View File

@ -23,6 +23,22 @@ import moe.fuqiuluo.shamrock.tools.jsonArray
import kotlin.math.abs import kotlin.math.abs
internal object MessageHelper { internal object MessageHelper {
suspend fun sendMessageWithoutMsgId(
chatType: Int,
peerId: String,
message: String,
callback: IOperateCallback,
fromId: String = peerId
): Pair<Long, Int> {
val uniseq = generateMsgId(chatType)
val msg = messageArrayToMessageElements(chatType, uniseq.second, peerId, decodeCQCode(message)).also {
if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。")
}.second.filter {
it.elementType != -1
} as ArrayList<MsgElement>
return sendMessageWithoutMsgId(chatType, peerId, msg, callback, fromId)
}
suspend fun sendMessageWithoutMsgId( suspend fun sendMessageWithoutMsgId(
chatType: Int, chatType: Int,
peerId: String, peerId: String,
@ -31,14 +47,31 @@ internal object MessageHelper {
fromId: String = peerId fromId: String = peerId
): Pair<Long, Int> { ): Pair<Long, Int> {
val uniseq = generateMsgId(chatType) val uniseq = generateMsgId(chatType)
var nonMsg: Boolean
val msg = messageArrayToMessageElements(chatType, uniseq.second, peerId, message).also { val msg = messageArrayToMessageElements(chatType, uniseq.second, 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
}.also { } as ArrayList<MsgElement>
nonMsg = it.isEmpty() return sendMessageWithoutMsgId(chatType, peerId, msg, callback, fromId)
} }
suspend fun sendMessageWithoutMsgId(
chatType: Int,
peerId: String,
message: ArrayList<MsgElement>,
callback: IOperateCallback,
fromId: String = peerId
): Pair<Long, Int> {
return sendMessageWithoutMsgId(generateContact(chatType, peerId, fromId), message, callback)
}
fun sendMessageWithoutMsgId(
contact: Contact,
message: ArrayList<MsgElement>,
callback: IOperateCallback
): Pair<Long, Int> {
val uniseq = generateMsgId(contact.chatType)
val nonMsg: Boolean = message.isEmpty()
return if (!nonMsg) { return if (!nonMsg) {
val service = QRoute.api(IMsgService::class.java) val service = QRoute.api(IMsgService::class.java)
if(callback is MsgSvc.MessageCallback) { if(callback is MsgSvc.MessageCallback) {
@ -46,9 +79,9 @@ internal object MessageHelper {
} }
service.sendMsg( service.sendMsg(
generateContact(chatType, peerId, fromId), contact,
uniseq.second, uniseq.second,
msg as ArrayList<MsgElement>, message,
callback callback
) )
System.currentTimeMillis() to uniseq.first System.currentTimeMillis() to uniseq.first
@ -57,6 +90,64 @@ internal object MessageHelper {
} }
} }
suspend fun sendMessageWithMsgId(
chatType: Int,
peerId: String,
message: JsonArray,
callback: IOperateCallback,
fromId: String = peerId
): Pair<Long, Int> {
val uniseq = generateMsgId(chatType)
val msg = messageArrayToMessageElements(chatType, uniseq.second, peerId, message).also {
if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。")
}.second.filter {
it.elementType != -1
} as ArrayList<MsgElement>
val contact = generateContact(chatType, peerId, fromId)
val nonMsg: Boolean = message.isEmpty()
return if (!nonMsg) {
val service = QRoute.api(IMsgService::class.java)
if(callback is MsgSvc.MessageCallback) {
callback.msgHash = uniseq.first
}
service.sendMsg(
contact,
uniseq.second,
msg,
callback
)
uniseq.second to uniseq.first
} else {
uniseq.second to 0
}
}
fun sendMessageWithMsgId(
contact: Contact,
message: ArrayList<MsgElement>,
callback: IOperateCallback
): Pair<Long, Int> {
val uniseq = generateMsgId(contact.chatType)
val nonMsg: Boolean = message.isEmpty()
return if (!nonMsg) {
val service = QRoute.api(IMsgService::class.java)
if(callback is MsgSvc.MessageCallback) {
callback.msgHash = uniseq.first
}
service.sendMsg(
contact,
uniseq.second,
message,
callback
)
uniseq.second to uniseq.first
} else {
0L to 0
}
}
suspend fun generateContact(chatType: Int, id: String, subId: String = ""): Contact { suspend fun generateContact(chatType: Int, id: String, subId: String = ""): Contact {
val peerId = if (MsgConstant.KCHATTYPEC2C == chatType || MsgConstant.KCHATTYPETEMPC2CFROMGROUP == chatType) { val peerId = if (MsgConstant.KCHATTYPEC2C == chatType || MsgConstant.KCHATTYPETEMPC2CFROMGROUP == chatType) {
ContactHelper.getUidByUinAsync(id.toLong()) ContactHelper.getUidByUinAsync(id.toLong())

View File

@ -1,22 +1,58 @@
package moe.fuqiuluo.shamrock.remote.action.handlers package moe.fuqiuluo.shamrock.remote.action.handlers
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.qqinterface.servlet.msg.convert.MessageConvert
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.remote.action.ActionSession import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher import moe.fuqiuluo.shamrock.remote.service.data.MessageDetail
import moe.fuqiuluo.shamrock.remote.service.data.MessageSender
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
internal object GetForwardMsg: IActionHandler() { internal object GetForwardMsg: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String { override suspend fun internalHandle(session: ActionSession): String {
val id = session.getString("id") val id = session.getString("id")
return invoke(id, session.echo)
val kernelService = NTServiceFetcher.kernelService
val sessionService = kernelService.wrapperSession
val msgService = sessionService.msgService
//msgService.getMultiMsg()
return error("不支持实现请提交ISSUE", session.echo)
} }
suspend operator fun invoke(
resId: String,
echo: JsonElement = EmptyJsonString
): String {
val result = MsgSvc.getMultiMsg(resId)
if (result.isFailure) {
return logic(result.exceptionOrNull().toString(), echo)
}
return ok(data = GetForwardMsgResult(result.getOrThrow().map { msg ->
val msgHash = MessageHelper.generateMsgIdHash(msg.chatType, msg.msgId)
MessageDetail(
time = msg.msgTime.toInt(),
msgType = MessageHelper.obtainDetailTypeByMsgType(msg.chatType),
msgId = msgHash,
realId = msg.msgSeq.toInt(),
sender = MessageSender(
msg.senderUin, msg.sendNickName, "unknown", 0, msg.senderUid
),
message = MessageConvert.convertMessageRecordToMsgSegment(msg).map {
it.toJson()
},
peerId = msg.peerUin,
groupId = if (msg.chatType == MsgConstant.KCHATTYPEGROUP) msg.peerUin else 0,
targetId = if (msg.chatType != MsgConstant.KCHATTYPEGROUP) msg.peerUin else 0
)
}), echo = echo)
}
@Serializable
data class GetForwardMsgResult(
@SerialName("messages") val msgs: List<MessageDetail>
)
override val requiredParams: Array<String> = arrayOf("id") override val requiredParams: Array<String> = arrayOf("id")
override fun path(): String = "get_forward_msg" override fun path(): String = "get_forward_msg"

View File

@ -1,41 +1,117 @@
package moe.fuqiuluo.shamrock.remote.action.handlers package moe.fuqiuluo.shamrock.remote.action.handlers
import com.tencent.qqnt.kernel.nativeinterface.MsgRecord import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MultiMsgInfo
import kotlinx.atomicfu.atomic
import kotlinx.serialization.json.JsonArray
import kotlinx.serialization.json.JsonElement import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
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.asInt import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import moe.fuqiuluo.qqinterface.servlet.MsgSvc import moe.fuqiuluo.qqinterface.servlet.msg.convert.toSegments
import moe.fuqiuluo.qqinterface.servlet.msg.LongMsgHelper import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.tools.EmptyJsonObject
import moe.fuqiuluo.shamrock.tools.EmptyJsonString import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.shamrock.tools.asInt
import moe.fuqiuluo.shamrock.tools.asJsonObject
import moe.fuqiuluo.shamrock.tools.asString
import moe.fuqiuluo.shamrock.tools.json
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
internal object SendGroupForwardMsg: IActionHandler() { internal object SendGroupForwardMsg: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String { override suspend fun internalHandle(session: ActionSession): String {
val groupId = session.getLong("group_id") val groupId = session.getString("group_id")
val hashList = session.getArrayOrNull("seqs")?.map { it.asInt } if (session.isArray("messages")) {
if (hashList != null) { val messages = session.getArray("messages")
val msgs = hashList.map { return invoke(messages, groupId, session.echo)
MsgSvc.getMsg(it).getOrNull()
} }
val resId = LongMsgHelper.uploadGroupMsg(groupId.toString(), msgs.filterNotNull()) return logic("未知格式合并转发消息", session.echo)
return ok(mapOf("res_id" to resId), session.echo)
} }
suspend operator fun invoke(
message: JsonArray,
groupId: String,
echo: JsonElement = EmptyJsonString
): String {
kotlin.runCatching {
val kernelService = NTServiceFetcher.kernelService
val sessionService = kernelService.wrapperSession
val msgService = sessionService.msgService
val selfUin = TicketSvc.getUin()
return "xxx" val msgs = message.map {
it.asJsonObject["data"].asJsonObject.let {
if (it.containsKey("content"))
MessageNode(it["name"].asString, it["content"])
else MessageIdNode(it["id"].asInt)
}
}.map {
if (it is MessageIdNode) {
val recordResult = MsgSvc.getMsg(it.id)
if (recordResult.isFailure) {
EmptyNode
} else {
val record = recordResult.getOrThrow()
MessageNode(
name = record.sendMemberName
.ifBlank { record.sendNickName }
.ifBlank { record.sendRemarkName }
.ifBlank { record.peerName },
content = record.toSegments().map { segment ->
segment.toJson()
}.json
)
}
} else {
it as MessageNode
}
}.filter {
it.content != null
} }
operator fun invoke(msgs: List<MsgRecord>, echo: JsonElement = EmptyJsonString): String { var forwardMsgCallback: (() -> Unit)? = null
if (msgs.isEmpty()) { val availableMsgSize = atomic(0)
return logic("消息为空", echo) val msgIds = msgs.map {
} else if (msgs.size > 100) { it.name to MessageHelper.sendMessageWithMsgId(MsgConstant.KCHATTYPEC2C, selfUin, it.content!!.let { msg ->
return logic("消息数量过多", echo) if (msg is JsonArray) msg else MessageHelper.decodeCQCode(msg.asString)
}, { _, _ ->
if (availableMsgSize.incrementAndGet() == msgs.size) {
forwardMsgCallback?.invoke()
}
}).first
} }
val from = MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, selfUin)
val to = MessageHelper.generateContact(MsgConstant.KCHATTYPEGROUP, groupId)
TODO() forwardMsgCallback = {
msgService.multiForwardMsg(ArrayList<MultiMsgInfo>(msgIds.size).apply {
msgIds.forEach { add(MultiMsgInfo(it.second, it.first)) }
}, from, to) { code, why ->
if (code != 0)
LogCenter.log("合并转发消息:$code($why)", Level.WARN)
} }
}
return ok(data = EmptyJsonObject, echo = echo)
}.onFailure {
return error("error: $it", echo)
}
return logic("合并转发消息失败(unknown error)", echo)
}
override val requiredParams: Array<String> = arrayOf("group_id")
override fun path(): String = "send_group_forward_msg" override fun path(): String = "send_group_forward_msg"
class MessageIdNode(
val id: Int
): Node
open class MessageNode(
val name: String,
val content: JsonElement?
): Node
object EmptyNode: MessageNode("", null)
interface Node
} }

View File

@ -22,6 +22,11 @@ import moe.fuqiuluo.shamrock.tools.isJsonData
import moe.fuqiuluo.shamrock.tools.isJsonString import moe.fuqiuluo.shamrock.tools.isJsonString
fun Routing.messageAction() { fun Routing.messageAction() {
getOrPost("/get_forward_msg") {
val id = fetchOrThrow("id")
call.respondText(GetForwardMsg(id))
}
getOrPost("/get_group_msg_history") { getOrPost("/get_group_msg_history") {
val peerId = fetchOrThrow("group_id") val peerId = fetchOrThrow("group_id")
val cnt = fetchOrNull("count")?.toInt() ?: 20 val cnt = fetchOrNull("count")?.toInt() ?: 20

View File

@ -16,9 +16,13 @@ import moe.fuqiuluo.shamrock.remote.service.api.GlobalEventTransmitter
import moe.fuqiuluo.shamrock.remote.service.data.push.MessageTempSource import moe.fuqiuluo.shamrock.remote.service.data.push.MessageTempSource
import moe.fuqiuluo.shamrock.remote.service.data.push.PostType import moe.fuqiuluo.shamrock.remote.service.data.push.PostType
import java.util.ArrayList import java.util.ArrayList
import java.util.HashMap import java.util.Collections
import kotlin.collections.HashMap
internal object AioListener: IKernelMsgListener { internal object AioListener: IKernelMsgListener {
// 通过MSG SEQ临时监听器
internal val messageLessListenerMap = Collections.synchronizedMap(HashMap<Long, MsgRecord.() -> Unit>())
override fun onRecvMsg(msgList: ArrayList<MsgRecord>) { override fun onRecvMsg(msgList: ArrayList<MsgRecord>) {
if (msgList.isEmpty()) return if (msgList.isEmpty()) return
@ -33,6 +37,15 @@ internal object AioListener: IKernelMsgListener {
try { try {
if (record.chatType == MsgConstant.KCHATTYPEGUILD) return // TODO: 频道消息暂不处理 if (record.chatType == MsgConstant.KCHATTYPEGUILD) return // TODO: 频道消息暂不处理
messageLessListenerMap.firstNotNullOfOrNull {
if(it.key == record.msgSeq) it else null
}?.let {
it.value(record)
messageLessListenerMap.remove(it.key)
}
if (record.msgSeq < 0) return
val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId) val msgHash = MessageHelper.generateMsgIdHash(record.chatType, record.msgId)
MessageHelper.saveMsgMapping( MessageHelper.saveMsgMapping(
@ -48,6 +61,10 @@ internal object AioListener: IKernelMsgListener {
val rawMsg = record.elements.toCQCode(record.chatType, record.peerUin.toString()) val rawMsg = record.elements.toCQCode(record.chatType, record.peerUin.toString())
if (rawMsg.isEmpty()) return if (rawMsg.isEmpty()) return
//if (rawMsg.contains("forward")) {
// LogCenter.log(record.extInfoForUI.decodeToString(), Level.WARN)
//}
when (record.chatType) { when (record.chatType) {
MsgConstant.KCHATTYPEGROUP -> { MsgConstant.KCHATTYPEGROUP -> {
LogCenter.log("群消息(group = ${record.peerName}(${record.peerUin}), uin = ${record.senderUin}, id = $msgHash|${record.msgSeq}, msg = $rawMsg)") LogCenter.log("群消息(group = ${record.peerName}(${record.peerUin}), uin = ${record.senderUin}, id = $msgHash|${record.msgSeq}, msg = $rawMsg)")
@ -63,7 +80,7 @@ internal object AioListener: IKernelMsgListener {
} }
} }
MsgConstant.KCHATTYPEC2C -> { MsgConstant.KCHATTYPEC2C -> {
LogCenter.log("私聊消息(private = ${record.senderUin}, id = $msgHash|${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 ->
if (rule.black?.contains(record.peerUin) == true) return if (rule.black?.contains(record.peerUin) == true) return
if (rule.white?.contains(record.peerUin) == false) return if (rule.white?.contains(record.peerUin) == false) return

View File

@ -8,6 +8,7 @@ import com.tencent.qphone.base.remote.ToServiceMsg
import com.tencent.qphone.base.util.CodecWarpper import com.tencent.qphone.base.util.CodecWarpper
import kotlinx.atomicfu.atomic import kotlinx.atomicfu.atomic
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import moe.fuqiuluo.shamrock.remote.service.PacketReceiver import moe.fuqiuluo.shamrock.remote.service.PacketReceiver
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY import moe.fuqiuluo.shamrock.tools.EMPTY_BYTE_ARRAY
@ -15,7 +16,10 @@ import moe.fuqiuluo.shamrock.tools.hookMethod
import moe.fuqiuluo.shamrock.tools.slice import moe.fuqiuluo.shamrock.tools.slice
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.utils.PlatformUtils import moe.fuqiuluo.shamrock.xposed.helper.internal.DynamicReceiver
import moe.fuqiuluo.shamrock.xposed.helper.internal.IPCRequest
private const val MAGIC_APP_ID = 114514
internal class HookWrapperCodec: IAction { internal class HookWrapperCodec: IAction {
private val IgnoredCmd = arrayOf( private val IgnoredCmd = arrayOf(
@ -54,12 +58,12 @@ internal class HookWrapperCodec: IAction {
val isInit = atomic(false) val isInit = atomic(false)
CodecWarpper::class.java.hookMethod("init").after { CodecWarpper::class.java.hookMethod("init").after {
if (isInit.value) return@after if (isInit.value) return@after
hookReceive(it.thisObject.javaClass) hookReceive(it.thisObject, it.thisObject.javaClass)
isInit.lazySet(true) isInit.lazySet(true)
} }
CodecWarpper::class.java.hookMethod("nativeOnReceData").before { CodecWarpper::class.java.hookMethod("nativeOnReceData").before {
if (isInit.value) return@before if (isInit.value) return@before
hookReceive(it.thisObject.javaClass) hookReceive(it.thisObject, it.thisObject.javaClass)
isInit.lazySet(true) isInit.lazySet(true)
} }
} catch (e: Exception) { } catch (e: Exception) {
@ -67,7 +71,26 @@ internal class HookWrapperCodec: IAction {
} }
} }
private fun hookReceive(thizClass: Class<*>) { private fun hookReceive(thiz: Any, thizClass: Class<*>) {
val onResponse = thizClass.getDeclaredMethod("onResponse", Integer.TYPE, Any::class.java, Integer.TYPE)
//LogCenter.log("HookWrapperCodec: onResponse = $onResponse", Level.INFO)
DynamicReceiver.register("fake_packet", IPCRequest {
val uin = it.getStringExtra("package_uin")!!
val cmd = it.getStringExtra("package_cmd")!!
val seq = it.getIntExtra("package_seq", 0)
val buffer = it.getByteArrayExtra("package_buffer")!!
//LogCenter.log("伪造收包(cmd = $cmd)")
val from = FromServiceMsg()
from.requestSsoSeq = seq
from.putWupBuffer(buffer)
from.serviceCmd = cmd
from.appId = MAGIC_APP_ID
from.setMsgSuccess()
from.uin = uin
from.appSeq = seq
onResponse.invoke(thiz, 0, from, 0)
})
thizClass.hookMethod("onResponse").before { thizClass.hookMethod("onResponse").before {
val from = it.args[1] as FromServiceMsg val from = it.args[1] as FromServiceMsg
try { try {
@ -88,7 +111,7 @@ internal class HookWrapperCodec: IAction {
} }
merge.BusiBuffVec.set(busiBufVec) merge.BusiBuffVec.set(busiBufVec)
from.putWupBuffer(merge.toByteArray()) from.putWupBuffer(merge.toByteArray())
} else { } else if (from.appId != MAGIC_APP_ID) {
if (from.serviceCmd in IgnoredCmd && ShamrockConfig.isInjectPacket()) { if (from.serviceCmd in IgnoredCmd && ShamrockConfig.isInjectPacket()) {
from.serviceCmd = "ShamrockInjectedCmd" from.serviceCmd = "ShamrockInjectedCmd"
from.putWupBuffer(EMPTY_BYTE_ARRAY) from.putWupBuffer(EMPTY_BYTE_ARRAY)