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

View File

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

View File

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

View File

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

View File

@ -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")
/**

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

View File

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

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

View File

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

View File

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

View File

@ -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)
}
val resId = LongMsgHelper.uploadGroupMsg(groupId.toString(), msgs.filterNotNull())
return ok(mapOf("res_id" to resId), 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()
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 {
if (msgs.isEmpty()) {
return logic("消息为空", echo)
} else if (msgs.size > 100) {
return logic("消息数量过多", 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
}
TODO()
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 logic("合并转发消息失败(unknown error)", echo)
}
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
}

View File

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

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.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

View File

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