mirror of
https://github.com/whitechi73/OpenShamrock.git
synced 2024-08-14 13:12:17 +08:00
Shamrock
: Support du chat de groupe et transfert de messages
This commit is contained in:
parent
d28c6dc820
commit
bc967cf926
@ -173,24 +173,19 @@ dependencies {
|
||||
implementation("io.coil-kt:coil: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("io.ktor:ktor-server-core:2.3.3")
|
||||
implementation("io.ktor:ktor-server-host-common:2.3.3")
|
||||
implementation("io.ktor:ktor-server-status-pages:2.3.3")
|
||||
implementation("io.ktor:ktor-server-netty:2.3.3")
|
||||
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.3")
|
||||
implementation("io.ktor:ktor-server-content-negotiation:2.3.3")
|
||||
implementation("io.ktor:ktor-client-core:2.3.3")
|
||||
implementation("io.ktor:ktor-client-cio:2.3.3")
|
||||
implementation("io.ktor:ktor-client-content-negotiation:2.3.3")
|
||||
implementation("io.ktor:ktor-serialization-kotlinx-json:2.3.3")
|
||||
// useless
|
||||
//implementation ("com.maxkeppeler.sheets-compose-dialogs:core:1.2.0")
|
||||
//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")
|
||||
|
||||
val ktorVersion = "2.3.3"
|
||||
implementation("io.ktor:ktor-server-core:$ktorVersion")
|
||||
implementation("io.ktor:ktor-server-host-common:$ktorVersion")
|
||||
implementation("io.ktor:ktor-server-status-pages:$ktorVersion")
|
||||
implementation("io.ktor:ktor-server-netty:$ktorVersion")
|
||||
implementation("io.ktor:ktor-server-content-negotiation:$ktorVersion")
|
||||
implementation("io.ktor:ktor-client-core:$ktorVersion")
|
||||
implementation("io.ktor:ktor-client-cio:$ktorVersion")
|
||||
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
|
||||
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
|
||||
//implementation("io.ktor:ktor-serialization-kotlinx-protobuf:$ktorVersion")
|
||||
|
||||
implementation(project(":xposed"))
|
||||
|
||||
|
@ -1,6 +1,50 @@
|
||||
package com.tencent.mobileqq.pb;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Arrays;
|
||||
|
||||
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) {
|
||||
return null;
|
||||
}
|
||||
|
@ -30,6 +30,10 @@ public interface IKernelMsgService {
|
||||
|
||||
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);
|
||||
|
||||
String createUidFromTinyId(long j2, long j3);
|
||||
|
@ -1,7 +1,5 @@
|
||||
package com.tencent.qqnt.kernel.nativeinterface;
|
||||
|
||||
/* compiled from: P */
|
||||
/* loaded from: classes2.dex */
|
||||
public final class MultiMsgInfo {
|
||||
long msgId;
|
||||
String senderShowName;
|
||||
@ -21,8 +19,6 @@ public final class MultiMsgInfo {
|
||||
return "MultiMsgInfo{msgId=" + this.msgId + ",senderShowName=" + this.senderShowName + ",}";
|
||||
}
|
||||
|
||||
public MultiMsgInfo(long j2, String str) {
|
||||
this.msgId = j2;
|
||||
this.senderShowName = str;
|
||||
public MultiMsgInfo(long msgId, String showName) {
|
||||
}
|
||||
}
|
||||
|
@ -74,6 +74,7 @@ dependencies {
|
||||
implementation("io.ktor:ktor-client-cio:$ktorVersion")
|
||||
implementation("io.ktor:ktor-client-content-negotiation:$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")
|
||||
|
||||
/**
|
||||
|
@ -0,0 +1,5 @@
|
||||
@file:OptIn(ExperimentalSerializationApi::class)
|
||||
package moe.fuqiuluo.qqinterface.entries
|
||||
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.Serializable
|
@ -101,7 +101,7 @@ internal abstract class BaseSvc {
|
||||
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)
|
||||
toServiceMsg.putWupBuffer(buffer)
|
||||
toServiceMsg.addAttribute("req_pb_protocol_flag", isPb)
|
||||
|
@ -1,3 +1,5 @@
|
||||
@file:OptIn(DelicateCoroutinesApi::class)
|
||||
|
||||
package moe.fuqiuluo.qqinterface.servlet
|
||||
|
||||
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.TempChatPrepareInfo
|
||||
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.time.withTimeoutOrNull
|
||||
import kotlinx.coroutines.withTimeoutOrNull
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import moe.fuqiuluo.proto.protobufOf
|
||||
import moe.fuqiuluo.shamrock.helper.ContactHelper
|
||||
import moe.fuqiuluo.shamrock.helper.Level
|
||||
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.xposed.helper.NTServiceFetcher
|
||||
import moe.fuqiuluo.shamrock.xposed.helper.msgService
|
||||
import java.util.UUID
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
@ -183,6 +192,44 @@ internal object MsgSvc: BaseSvc() {
|
||||
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(
|
||||
private val peerId: String,
|
||||
var msgHash: Int
|
||||
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
||||
}
|
@ -53,8 +53,8 @@ internal object MessageConvert {
|
||||
MsgConstant.KELEMTYPEREPLY to MessageElemConverter.ReplyConverter,
|
||||
MsgConstant.KELEMTYPEGRAYTIP to MessageElemConverter.GrayTipsConverter,
|
||||
MsgConstant.KELEMTYPEFILE to MessageElemConverter.FileConverter,
|
||||
MsgConstant.KELEMTYPEMULTIFORWARD to MessageElemConverter.XmlMultiMsgConverter,
|
||||
MsgConstant.KELEMTYPESTRUCTLONGMSG to MessageElemConverter.XmlLongMsgConverter,
|
||||
//MsgConstant.KELEMTYPEMULTIFORWARD to MessageElemConverter.XmlMultiMsgConverter,
|
||||
//MsgConstant.KELEMTYPESTRUCTLONGMSG to MessageElemConverter.XmlLongMsgConverter,
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -23,6 +23,22 @@ import moe.fuqiuluo.shamrock.tools.jsonArray
|
||||
import kotlin.math.abs
|
||||
|
||||
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(
|
||||
chatType: Int,
|
||||
peerId: String,
|
||||
@ -31,14 +47,31 @@ internal object MessageHelper {
|
||||
fromId: String = peerId
|
||||
): Pair<Long, Int> {
|
||||
val uniseq = generateMsgId(chatType)
|
||||
var nonMsg: Boolean
|
||||
val msg = messageArrayToMessageElements(chatType, uniseq.second, peerId, message).also {
|
||||
if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。")
|
||||
}.second.filter {
|
||||
it.elementType != -1
|
||||
}.also {
|
||||
nonMsg = it.isEmpty()
|
||||
}
|
||||
} as ArrayList<MsgElement>
|
||||
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) {
|
||||
val service = QRoute.api(IMsgService::class.java)
|
||||
if(callback is MsgSvc.MessageCallback) {
|
||||
@ -46,9 +79,9 @@ internal object MessageHelper {
|
||||
}
|
||||
|
||||
service.sendMsg(
|
||||
generateContact(chatType, peerId, fromId),
|
||||
contact,
|
||||
uniseq.second,
|
||||
msg as ArrayList<MsgElement>,
|
||||
message,
|
||||
callback
|
||||
)
|
||||
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 {
|
||||
val peerId = if (MsgConstant.KCHATTYPEC2C == chatType || MsgConstant.KCHATTYPETEMPC2CFROMGROUP == chatType) {
|
||||
ContactHelper.getUidByUinAsync(id.toLong())
|
||||
|
@ -1,22 +1,58 @@
|
||||
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.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() {
|
||||
override suspend fun internalHandle(session: ActionSession): String {
|
||||
val id = session.getString("id")
|
||||
|
||||
val kernelService = NTServiceFetcher.kernelService
|
||||
val sessionService = kernelService.wrapperSession
|
||||
val msgService = sessionService.msgService
|
||||
|
||||
//msgService.getMultiMsg()
|
||||
|
||||
return error("不支持实现,请提交ISSUE!", session.echo)
|
||||
return invoke(id, 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 fun path(): String = "get_forward_msg"
|
||||
|
@ -1,41 +1,117 @@
|
||||
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 moe.fuqiuluo.qqinterface.servlet.MsgSvc
|
||||
import moe.fuqiuluo.shamrock.remote.action.ActionSession
|
||||
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
|
||||
import moe.fuqiuluo.shamrock.tools.asInt
|
||||
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
|
||||
import moe.fuqiuluo.qqinterface.servlet.msg.LongMsgHelper
|
||||
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
|
||||
import moe.fuqiuluo.qqinterface.servlet.msg.convert.toSegments
|
||||
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.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() {
|
||||
override suspend fun internalHandle(session: ActionSession): String {
|
||||
val groupId = session.getLong("group_id")
|
||||
val hashList = session.getArrayOrNull("seqs")?.map { it.asInt }
|
||||
if (hashList != null) {
|
||||
val msgs = hashList.map {
|
||||
MsgSvc.getMsg(it).getOrNull()
|
||||
val groupId = session.getString("group_id")
|
||||
if (session.isArray("messages")) {
|
||||
val messages = session.getArray("messages")
|
||||
return invoke(messages, groupId, session.echo)
|
||||
}
|
||||
return logic("未知格式合并转发消息", 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()
|
||||
|
||||
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
|
||||
}
|
||||
val resId = LongMsgHelper.uploadGroupMsg(groupId.toString(), msgs.filterNotNull())
|
||||
return ok(mapOf("res_id" to resId), session.echo)
|
||||
|
||||
var forwardMsgCallback: (() -> Unit)? = null
|
||||
val availableMsgSize = atomic(0)
|
||||
val msgIds = msgs.map {
|
||||
it.name to MessageHelper.sendMessageWithMsgId(MsgConstant.KCHATTYPEC2C, selfUin, it.content!!.let { msg ->
|
||||
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)
|
||||
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 "xxx"
|
||||
return logic("合并转发消息失败(unknown error)", echo)
|
||||
}
|
||||
|
||||
operator fun invoke(msgs: List<MsgRecord>, echo: JsonElement = EmptyJsonString): String {
|
||||
if (msgs.isEmpty()) {
|
||||
return logic("消息为空", echo)
|
||||
} else if (msgs.size > 100) {
|
||||
return logic("消息数量过多", echo)
|
||||
}
|
||||
|
||||
|
||||
|
||||
TODO()
|
||||
}
|
||||
override val requiredParams: Array<String> = arrayOf("group_id")
|
||||
|
||||
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
|
||||
}
|
@ -22,6 +22,11 @@ import moe.fuqiuluo.shamrock.tools.isJsonData
|
||||
import moe.fuqiuluo.shamrock.tools.isJsonString
|
||||
|
||||
fun Routing.messageAction() {
|
||||
getOrPost("/get_forward_msg") {
|
||||
val id = fetchOrThrow("id")
|
||||
call.respondText(GetForwardMsg(id))
|
||||
}
|
||||
|
||||
getOrPost("/get_group_msg_history") {
|
||||
val peerId = fetchOrThrow("group_id")
|
||||
val cnt = fetchOrNull("count")?.toInt() ?: 20
|
||||
|
@ -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.PostType
|
||||
import java.util.ArrayList
|
||||
import java.util.HashMap
|
||||
import java.util.Collections
|
||||
import kotlin.collections.HashMap
|
||||
|
||||
internal object AioListener: IKernelMsgListener {
|
||||
// 通过MSG SEQ临时监听器
|
||||
internal val messageLessListenerMap = Collections.synchronizedMap(HashMap<Long, MsgRecord.() -> Unit>())
|
||||
|
||||
override fun onRecvMsg(msgList: ArrayList<MsgRecord>) {
|
||||
if (msgList.isEmpty()) return
|
||||
|
||||
@ -33,6 +37,15 @@ internal object AioListener: IKernelMsgListener {
|
||||
try {
|
||||
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)
|
||||
|
||||
MessageHelper.saveMsgMapping(
|
||||
@ -48,6 +61,10 @@ internal object AioListener: IKernelMsgListener {
|
||||
val rawMsg = record.elements.toCQCode(record.chatType, record.peerUin.toString())
|
||||
if (rawMsg.isEmpty()) return
|
||||
|
||||
//if (rawMsg.contains("forward")) {
|
||||
// LogCenter.log(record.extInfoForUI.decodeToString(), Level.WARN)
|
||||
//}
|
||||
|
||||
when (record.chatType) {
|
||||
MsgConstant.KCHATTYPEGROUP -> {
|
||||
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 -> {
|
||||
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 ->
|
||||
if (rule.black?.contains(record.peerUin) == true) return
|
||||
if (rule.white?.contains(record.peerUin) == false) return
|
||||
|
@ -8,6 +8,7 @@ import com.tencent.qphone.base.remote.ToServiceMsg
|
||||
import com.tencent.qphone.base.util.CodecWarpper
|
||||
import kotlinx.atomicfu.atomic
|
||||
import kotlinx.coroutines.DelicateCoroutinesApi
|
||||
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
|
||||
import moe.fuqiuluo.shamrock.remote.service.PacketReceiver
|
||||
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
|
||||
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.helper.Level
|
||||
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 {
|
||||
private val IgnoredCmd = arrayOf(
|
||||
@ -54,12 +58,12 @@ internal class HookWrapperCodec: IAction {
|
||||
val isInit = atomic(false)
|
||||
CodecWarpper::class.java.hookMethod("init").after {
|
||||
if (isInit.value) return@after
|
||||
hookReceive(it.thisObject.javaClass)
|
||||
hookReceive(it.thisObject, it.thisObject.javaClass)
|
||||
isInit.lazySet(true)
|
||||
}
|
||||
CodecWarpper::class.java.hookMethod("nativeOnReceData").before {
|
||||
if (isInit.value) return@before
|
||||
hookReceive(it.thisObject.javaClass)
|
||||
hookReceive(it.thisObject, it.thisObject.javaClass)
|
||||
isInit.lazySet(true)
|
||||
}
|
||||
} 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 {
|
||||
val from = it.args[1] as FromServiceMsg
|
||||
try {
|
||||
@ -88,7 +111,7 @@ internal class HookWrapperCodec: IAction {
|
||||
}
|
||||
merge.BusiBuffVec.set(busiBufVec)
|
||||
from.putWupBuffer(merge.toByteArray())
|
||||
} else {
|
||||
} else if (from.appId != MAGIC_APP_ID) {
|
||||
if (from.serviceCmd in IgnoredCmd && ShamrockConfig.isInjectPacket()) {
|
||||
from.serviceCmd = "ShamrockInjectedCmd"
|
||||
from.putWupBuffer(EMPTY_BYTE_ARRAY)
|
||||
|
Loading…
x
Reference in New Issue
Block a user