3 Commits

Author SHA1 Message Date
92ebe0c6a8 send_forward_msg(support markdown, button...) 2024-02-25 04:10:29 +08:00
6b1147d065 Shamrock: fix #252
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-02-25 03:38:09 +08:00
720313124c Shamrock: support requestUploadGroupPic
Signed-off-by: 白池 <whitechi73@outlook.com>
2024-02-25 03:27:55 +08:00
45 changed files with 1565 additions and 1131 deletions

View File

@ -199,17 +199,36 @@
-keep class com.arthenica.ffmpegkit.NativeLoader { *; }
-keep class moe.fuqiuluo.** { *; }
-keep class moe.fuqiuluo.shamrock.app.** { *; }
-keep class moe.fuqiuluo.shamrock.ui.** { *; }
-keep class moe.fuqiuluo.shamrock.MainActivity { *; }
-keep class moe.fuqiuluo.shamrock.MainActivityKt { *; }
-keep class moe.fuqiuluo.shamrock.Manifest { *; }
-keep class moe.fuqiuluo.shamrock.xposed.** { *; }
-keep class moe.fuqiuluo.shamrock.helper.** { *; }
# tencent interfaces rules
-keep class com.tencent.** { *; }
-keep class com.qq.** { *; }
-keep class com.google.gson.** { *; }
-keep class de.** { *; }
-keep class epic.** { *; }
-keep class friendlist.** { *; }
-keep class KQQ.** { *; }
-keep class mqq.** { *; }
-keep class msf.** { *; }
-keep class oicq.** { *; }
-keep class QQService.** { *; }
-keep class SummaryCard.** { *; }
-keep class tencent.** { *; }
-keep class VIP.** { *; }
-keepclassmembers class * {
native <methods>;
}
-keep class io.netty.** { *; }
-keepclasseswithmembernames class * {
native <methods>;
}

View File

@ -2,16 +2,14 @@ package moe.fuqiuluo.shamrock
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
fun test() {
}
}

View File

@ -15,7 +15,7 @@ data class ContentHead(
@ProtoNumber(8) val u6: Int? = null,
@ProtoNumber(9) val u7: Int? = null,
@ProtoNumber(11) val msgSeq: Long? = null,
@ProtoNumber(12) val msgRandom: Long = Long.MIN_VALUE,
@ProtoNumber(12) val msgRandom: Long = Long.MIN_VALUE, // 0x0100000000000000L xor msgViaRandom
@ProtoNumber(14) val u4: Long? = null,
@ProtoNumber(15) val forwardHead: ForwardHead? = null,
@ProtoNumber(28) val u5: Long? = null

View File

@ -30,7 +30,7 @@ data class CustomFace(
@ProtoNumber(23) var height: UInt? = null,
@ProtoNumber(24) var source: UInt? = null,
@ProtoNumber(25) var size: UInt? = null,
@ProtoNumber(26) var origin: UInt? = null,
@ProtoNumber(26) var origin: Boolean? = null,
@ProtoNumber(27) var thumbWidth: UInt? = null,
@ProtoNumber(28) var thumbHeight: UInt? = null,
@ProtoNumber(29) var showLen: UInt? = null,

View File

@ -5,8 +5,8 @@ import kotlinx.serialization.protobuf.ProtoNumber
@Serializable
data class FaceMsg(
@ProtoNumber(1) val id: Int? = null,
@ProtoNumber(1) val index: Int? = null,
@ProtoNumber(2) var old: ByteArray? = null,
@ProtoNumber(11) var buf: ByteArray? = null,
)
)

View File

@ -12,7 +12,7 @@ data class GeneralFlags(
@ProtoNumber(4) val rpId: ByteArray? = null,
@ProtoNumber(5) val prpFold: UInt? = null,
@ProtoNumber(6) val longTextFlag: UInt? = null,
@ProtoNumber(7) val longTextResid: ByteArray? = null,
@ProtoNumber(7) val longTextResid: String? = null,
@ProtoNumber(8) val groupType: UInt? = null,
@ProtoNumber(9) val toUinFlag: UInt? = null,
@ProtoNumber(10) val glamourLevel: UInt? = null,

View File

@ -14,10 +14,10 @@ data class NotOnlineImage(
@ProtoNumber(7) val picMd5: ByteArray? = null,
@ProtoNumber(8) val picHeight: UInt? = null,
@ProtoNumber(9) val picWidth: UInt? = null,
@ProtoNumber(10) val resId: ByteArray? = null,
@ProtoNumber(10) val resId: ByteArray? = null, // md5 + ".jpg"
@ProtoNumber(11) val flag: ByteArray? = null,
@ProtoNumber(12) val thumbUrl: String? = null,
@ProtoNumber(13) val original: UInt? = null,
@ProtoNumber(13) val original: Boolean? = null,
@ProtoNumber(14) val bigUrl: String? = null,
@ProtoNumber(15) val origUrl: String? = null,
@ProtoNumber(16) val bizType: UInt? = null,

View File

@ -21,7 +21,7 @@ data class SourceMsg(
companion object {
@Serializable
data class PbReserve(
@ProtoNumber(3) var field3: ULong? = null,
@ProtoNumber(3) var msgRand: ULong? = null,
@ProtoNumber(6) var senderUid: String? = null,
@ProtoNumber(7) var receiverUid: String? = null,
@ProtoNumber(8) var field8: Int? = null,

View File

@ -10,5 +10,12 @@ data class TextMsg(
@ProtoNumber(3) val attr6Buf: ByteArray? = null,
@ProtoNumber(4) val attr7Buf: ByteArray? = null,
@ProtoNumber(11) val buf: ByteArray? = null,
@ProtoNumber(12) val pbReserve: ByteArray? = null,
)
@ProtoNumber(12) val pbReserve: PbReserve? = null,
){
companion object {
@Serializable
data class PbReserve(
@ProtoNumber(1) val field1: String? = null, // [打 call]] 请使用最新版手机 QQ 体验新功能
)
}
}

View File

@ -23,7 +23,7 @@ data class Row(
@Serializable
data class Button(
@ProtoNumber(1) val id: Int? = null,
@ProtoNumber(1) val id: String? = null,
@ProtoNumber(2) val renderData: RenderData? = null,
@ProtoNumber(3) val action: Action? = null,
)
@ -41,8 +41,8 @@ data class Action(
@ProtoNumber(2) val permission: Permission? = null,
@ProtoNumber(4) val unsupportTips: String? = null,
@ProtoNumber(5) val data: String? = null,
@ProtoNumber(6) val reply: Boolean? = null,
@ProtoNumber(7) val enter: Boolean? = null,
@ProtoNumber(7) val reply: Boolean? = null,
@ProtoNumber(8) val enter: Boolean? = null,
)
@Serializable

View File

@ -11,7 +11,7 @@ data class QFaceExtra(
@ProtoNumber(3) val faceId: Int? = null,
@ProtoNumber(4) val field4: Int? = null,
@ProtoNumber(5) val field5: Int? = null,
@ProtoNumber(6) val field6: String? = null,
@ProtoNumber(6) val result: String? = null,
@ProtoNumber(7) val faceText: String? = null,
@ProtoNumber(9) val field9: Int? = null
) : Protobuf<QFaceExtra>

View File

@ -0,0 +1,63 @@
package protobuf.oidb.cmd0x388
import com.google.protobuf.Internal.EMPTY_BYTE_ARRAY
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
import moe.fuqiuluo.symbols.Protobuf
@Serializable
data class Cmd0x388ReqBody(
@ProtoNumber(1) var netType: Int = 0,
@ProtoNumber(2) var subCmd: Int = 0,
@ProtoNumber(3) var msgTryUpImg: ArrayList<TryUpImgReq>? = null,
// @ProtoNumber(4) var rpt_msg_getimg_url_req: ArrayList<GetImgUrlReq>? = null,
@ProtoNumber(5) var msgTryUpPttReq: ArrayList<TryUpPttReq>? = null,
// @ProtoNumber(6) var msgGetPttUrlReq: ArrayList<GetPttUrlReq>? = null,
@ProtoNumber(7) var commandId: Int = 0,
// @ProtoNumber(8) var rpt_msg_del_img_req: ArrayList<DelImgReq>? = null,
@ProtoNumber(1001) var extension: ByteArray = EMPTY_BYTE_ARRAY,
): Protobuf<Cmd0x388ReqBody>
@Serializable
data class TryUpImgReq(
@ProtoNumber(1) var groupCode: Long = 0,
@ProtoNumber(2) var srcUin: Long = 0,
@ProtoNumber(3) var fileId: Long? = null,
@ProtoNumber(4) var fileMd5: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(5) var fileSize: Long = 0,
@ProtoNumber(6) var fileName: String = "",
@ProtoNumber(7) var srcTerm: Int = 0,
@ProtoNumber(8) var platformType: Int = 0,
@ProtoNumber(9) var buType: Int = 0,
@ProtoNumber(10) var picWidth: Int = 0,
@ProtoNumber(11) var picHeight: Int = 0,
@ProtoNumber(12) var picType: Int = 0,
@ProtoNumber(13) var buildVer: String = "",
@ProtoNumber(14) var innerIp: Int = 0,
@ProtoNumber(15) var appPicType: Int = 0,
@ProtoNumber(16) var originalPic: Int = 0,
@ProtoNumber(17) var fileIndex: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(18) var dstUin: Long = 0,
@ProtoNumber(19) var srvUpload: Int? = null,
@ProtoNumber(20) var transferUrl: ByteArray = EMPTY_BYTE_ARRAY,
)
@Serializable
data class TryUpPttReq(
@ProtoNumber(1) var groupCode: Long = 0,
@ProtoNumber(2) var srcUin: Long = 0,
@ProtoNumber(3) var fileId: Long = 0,
@ProtoNumber(4) var fileMd5: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(5) var fileSize: Long = 0,
@ProtoNumber(6) var fileName: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(7) var srcTerm: Int = 0,
@ProtoNumber(8) var platformType: Int = 0,
@ProtoNumber(9) var buType: Int = 0,
@ProtoNumber(10) var buildVer: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(11) var innerIp: Int = 0,
@ProtoNumber(12) var voiceLength: Int = 0,
@ProtoNumber(13) var newUpChan: Boolean = false,
@ProtoNumber(14) var codec: Int = 0,
@ProtoNumber(15) var voiceType: Int = 0,
@ProtoNumber(16) var buId: Int = 0,
)

View File

@ -0,0 +1,78 @@
@file:OptIn(ExperimentalSerializationApi::class)
package protobuf.oidb.cmd0x388
import com.google.protobuf.Internal.EMPTY_BYTE_ARRAY
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
import moe.fuqiuluo.symbols.Protobuf
@Serializable
data class Cmd0x388RspBody(
@ProtoNumber(1) var clientIp: UInt = 0u,
@ProtoNumber(2) var subCmd: UInt = 0u,
@ProtoNumber(3) var msgTryUpImgRsp: ArrayList<TryUpImgRsp>? = null,
@ProtoNumber(5) var msgTryUpPttRsp: ArrayList<TryUpPttRsp>? = null,
): Protobuf<Cmd0x388RspBody>
@Serializable
data class TryUpPttRsp(
@ProtoNumber(1) var fileId: Long = 0,
@ProtoNumber(2) var result: Int = 0,
@ProtoNumber(3) var failMsg: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(4) var fileExit: Boolean = false,
@ProtoNumber(5) var upIp: List<Int>? = null,
@ProtoNumber(6) var upPort: List<Int>? = null,
@ProtoNumber(7) var upUkey: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(8) var fileid: Long = 0,
@ProtoNumber(9) var upOffset: Long = 0,
@ProtoNumber(10) var blockSize: Long = 0,
@ProtoNumber(11) var fileKey: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(12) var channelType: Int = 0,
@ProtoNumber(26) var msgUpIp6: ArrayList<IPv6Info>? = null,
@ProtoNumber(27) var clientIp6: ByteArray = EMPTY_BYTE_ARRAY,
): Protobuf<TryUpPttRsp>
@Serializable
data class TryUpImgRsp(
@ProtoNumber(1) var extFileId: ULong = 0u, // 没有实际作用
@ProtoNumber(2) var result: UInt = 0u,
@ProtoNumber(3) var faiMsg: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(4) var fileExist: Boolean = false,
@ProtoNumber(5) var msgImgInfo: ImgInfo? = null, // 里面只是一堆垃圾
@ProtoNumber(6) var upIp: ArrayList<Long>? = null,
@ProtoNumber(7) var upPort: ArrayList<Int>? = null,
@ProtoNumber(8) var ukey: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(9) var fileId: Long = 0,
@ProtoNumber(10) var upOffset: ULong = 0u,
@ProtoNumber(11) var blockSize: Long = 0,
@ProtoNumber(12) var bool_new_big_chan: Boolean = false,
@ProtoNumber(26) var rpt_msg_up_ip6: ArrayList<IPv6Info>? = null,
@ProtoNumber(27) var client_ip6: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(1001) var msg_info4busi: TryUpInfo4Busi? = null,
)
@Serializable
data class TryUpInfo4Busi(
@ProtoNumber(1) var down_domain: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(2) var thumb_down_url: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(3) var original_down_url: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(4) var big_down_url: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(5) var file_resid: ByteArray = EMPTY_BYTE_ARRAY,
)
@Serializable
data class IPv6Info(
@ProtoNumber(1) var ip6: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(2) var port: UInt = 0u,
)
@Serializable
data class ImgInfo(
@ProtoNumber(1) var file_md5: ByteArray = EMPTY_BYTE_ARRAY,
@ProtoNumber(2) var file_type: UInt = 0u,
@ProtoNumber(3) var file_size: ULong = 0u,
@ProtoNumber(4) var file_width: UInt = 0u,
@ProtoNumber(5) var file_height: UInt = 0u,
)

View File

@ -0,0 +1,74 @@
package com.tencent.mobileqq.data;
import java.util.HashMap;
import java.util.Map;
public class MessageForPic extends MessageRecord {
public String SpeedInfo;
public String actMsgContentValue;
public String action;
public String bigMsgUrl;
public String bigThumbMsgUrl;
public int busiType;
public int fileSizeFlag;
public long groupFileID;
public long height;
public int imageType;
public boolean isInMixedMsg;
public boolean isMixed;
public boolean isRead;
public boolean isShareAppActionMsg;
public String localUUID;
public int mCurrlength;
public int mDownloadLength;
public long mPresendTransferedSize;
public int mShowLength;
public String md5;
//@RecordForTest
// public msg_ctrl$MsgCtrl msgCtrl;
public int msgVia;
public int ntChatType;
public String ntPeerUid;
public String path;
//public PicMessageExtraData picExtraData;
public int picExtraFlag;
public Object picExtraObject;
public int previewed;
public String rawMsgUrl;
/// public ReportInfo reportInfo;
//public MsgRecordParams rootMsgRecordParams;
public String serverStoreSource;
public long shareAppID;
public long size;
public long subTypeId;
public int thumbHeight;
public String thumbMsgUrl;
public int thumbWidth;
//public ThumbWidthHeightDP thumbWidthHeightDP;
public int type;
public String uuid;
public long width;
public boolean isDownStatusReady = false;
public int subMsgId = 0;
public int isReport = 0;
public int subVersion = 5;
public int preDownState = -1;
public int preDownNetworkType = -1;
public long DSKey = 0;
public int mNotPredownloadReason = 0;
public int subThumbWidth = -1;
public int subThumbHeight = -1;
public int aiofileType = -1;
public int subMsgType = -1;
public boolean bEnableEnc = false;
public int thumbSize = -1;
public boolean isBlessPic = false;
public boolean sync2Story = false;
public boolean isQzonePic = false;
public boolean isStoryPhoto = false;
public long replyRealSourceMsgId = -1;
public String toLogString() {
return "path:" + this.path + ",uuid:" + this.uuid + ",md5:" + this.md5 + ",size:" + this.size + ",groupFileID:" + this.groupFileID;
}
}

View File

@ -1,6 +1,8 @@
package com.tencent.mobileqq.transfile;
public class BaseTransProcessor {
import com.tencent.mobileqq.utils.httputils.IHttpCommunicatorListener;
public class BaseTransProcessor implements IHttpCommunicatorListener {
public FileMsg file;
public long getFileStatus() {

View File

@ -0,0 +1,5 @@
package com.tencent.mobileqq.transfile;
public class BaseUploadProcessor extends BaseTransProcessor {
}

View File

@ -1,5 +1,9 @@
package com.tencent.mobileqq.transfile;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
public class FileMsg {
public static final int STATUS_FILE_EXPIRED = 5002;
public static final int STATUS_FILE_TRANSFERING = 5000;
@ -107,4 +111,58 @@ public class FileMsg {
public static final int UIN_BUDDY = 0;
public static final int UIN_DISCUSS = 2;
public static final int UIN_TROOP = 1;
public String domain;
public String downDomain;
public long endTime;
public int errorCode;
public String errorMessage;
public File file;
public long fileID;
public String fileKey;
public String fileMd5;
public String filePath;
public long fileSize;
public int fileType;
public String fileUrl;
public String forwardTaskKey;
public String friendUin;
public int isRead;
public boolean isStreamMode;
public int lastStatus;
public byte[] localFileMd5;
public String logTag;
public long mSecMsgId;
public long mSubMsgId;
public String mUin;
public String msgImageData;
public String msgTime;
public String orgiDownUrl;
public String peerUin;
public int picScale;
public long picThumbSize;
public BaseTransProcessor processor;
public boolean processorDoReportSelf;
public int pttSlicePos;
public String pttSliceText;
public OutputStream revStream;
public String selfUin;
public InputStream sendStream;
public String serverPath;
public long startTime;
public int status;
public long stepUrlCost;
public String suffixType;
public String thumbDownUrl;
public String thumbPath;
public String thumbUrl;
public String tmpFilePath;
public byte[] transferData;
public long transferedSize;
public String uKey;
public int uinType;
public long uniseq;
public String[] urls;
public byte[] userInfo;
public String uuidPath;
}

View File

@ -1,4 +1,6 @@
package com.tencent.mobileqq.transfile;
public class GroupPicUploadProcessor {
import com.tencent.mobileqq.utils.httputils.IHttpCommunicatorListener;
public class GroupPicUploadProcessor extends BaseUploadProcessor {
}

View File

@ -208,6 +208,8 @@ public final class PicElement implements IKernelModel {
this.isFlashPic = bool;
}
public void setStoreID(int i2) {
}
public void setMd5HexStr(String str) {
this.md5HexStr = str;
}

View File

@ -18,182 +18,4 @@
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
# Never inline methods, but allow shrinking and obfuscation.
-keepclassmembernames,allowobfuscation,allowshrinking class androidx.core.view.ViewCompat$Api* {
<methods>;
}
-keepclassmembernames,allowobfuscation,allowshrinking class androidx.core.view.WindowInsetsCompat$*Impl* {
<methods>;
}
-keepclassmembernames,allowobfuscation,allowshrinking class androidx.core.app.NotificationCompat$*$Api*Impl {
<methods>;
}
-keepclassmembernames,allowobfuscation,allowshrinking class androidx.core.os.UserHandleCompat$Api*Impl {
<methods>;
}
-keepclassmembernames,allowobfuscation,allowshrinking class androidx.core.widget.EdgeEffectCompat$Api*Impl {
<methods>;
}
# Keep metadata about inner emulated classes. This helps with
# reflection on older platforms, but can be omitted if the
# metadata usage is not present in the app.
-keepclassmembers class * {
** CREATOR;
}
# Keep the special static methods that are required in all enumeration
# classes.
-keepclassmembers enum * {
public static **[] values();
public static ** valueOf(java.lang.String);
}
-keep public class androidx.appcompat.widget.** { *; }
-keep public class androidx.appcompat.app.** { *; }
-keep public class androidx.appcompat.view.menu.** { *; }
# Ensure that reflectively-loaded inflater is not obfuscated. This can be
# removed when we stop supporting AAPT1 builds.
-keepnames class androidx.appcompat.app.AppCompatViewInflater
# aapt is not able to read app::actionViewClass and app:actionProviderClass to produce proguard
# keep rules. Add a commonly used SearchView to the keep list until b/109831488 is resolved.
-keep class androidx.appcompat.widget.SearchView { <init>(...); }
# CoordinatorLayout resolves the behaviors of its child components with reflection.
-keep public class * extends androidx.coordinatorlayout.widget.CoordinatorLayout$Behavior {
public <init>(android.content.Context, android.util.AttributeSet);
public <init>();
}
# Make sure we keep annotations for CoordinatorLayout's DefaultBehavior
-keepattributes RuntimeVisible*Annotation*
-if class androidx.appcompat.app.AppCompatViewInflater
-keep class com.google.android.material.theme.MaterialComponentsViewInflater {
<init>();
}
# Keep `Companion` object fields of serializable classes.
# This avoids serializer lookup through `getDeclaredClasses` as done for named companion objects.
-if @kotlinx.serialization.Serializable class **
-keepclassmembers class <1> {
static <1>$Companion Companion;
}
# Keep `serializer()` on companion objects (both default and named) of serializable classes.
-if @kotlinx.serialization.Serializable class ** {
static **$* *;
}
-keepclassmembers class <2>$<3> {
kotlinx.serialization.KSerializer serializer(...);
}
# Keep `INSTANCE.serializer()` of serializable objects.
-if @kotlinx.serialization.Serializable class ** {
public static ** INSTANCE;
}
-keepclassmembers class <1> {
public static <1> INSTANCE;
kotlinx.serialization.KSerializer serializer(...);
}
# @Serializable and @Polymorphic are used at runtime for polymorphic serialization.
-keepattributes RuntimeVisibleAnnotations,AnnotationDefault
# Don't print notes about potential mistakes or omissions in the configuration for kotlinx-serialization classes
# See also https://github.com/Kotlin/kotlinx.serialization/issues/1900
-dontnote kotlinx.serialization.**
# Serialization core uses `java.lang.ClassValue` for caching inside these specified classes.
# If there is no `java.lang.ClassValue` (for example, in Android), then R8/ProGuard will print a warning.
# However, since in this case they will not be used, we can disable these warnings
-dontwarn kotlinx.serialization.internal.ClassValueReferences
# Rule to save runtime annotations on serializable class.
# If the R8 full mode is used, annotations are removed from classes-files.
#
# For the annotation serializer, it is necessary to read the `Serializable` annotation inside the serializer<T>() function - if it is present,
# then `SealedClassSerializer` is used, if absent, then `PolymorphicSerializer'.
#
# When using R8 full mode, all interfaces will be serialized using `PolymorphicSerializer`.
#
# see https://github.com/Kotlin/kotlinx.serialization/issues/2050
-if @kotlinx.serialization.Serializable class **
-keep, allowshrinking, allowoptimization, allowobfuscation, allowaccessmodification class <1>
# Entry point for retaining MainDispatcherLoader which uses a ServiceLoader.
-keep class kotlinx.coroutines.Dispatchers {
** getMain();
}
# Entry point for retaining CoroutineExceptionHandlerImpl.handlers which uses a ServiceLoader.
-keep class kotlinx.coroutines.CoroutineExceptionHandlerKt {
void handleCoroutineException(...);
}
# Entry point for the rest of coroutines machinery
-keep class kotlinx.coroutines.BuildersKt {
** runBlocking(...);
** launch(...);
}
# We are cheating a bit by not having android.jar on R8's library classpath. Ignore those warnings.
-ignorewarnings
-keep class kotlinx.coroutines.android.AndroidDispatcherFactory {*;}
-keep class kotlinx.coroutines.android.AndroidExceptionPreHandler {*;}
# Statically turn off all debugging facilities and assertions
-keepclassmembers class io.ktor.** { volatile <fields>; }
-keep class io.ktor.** { *; }
-keep class kotlinx.coroutines.** { *; }
-dontwarn kotlinx.atomicfu.**
-dontwarn io.netty.**
-dontwarn com.typesafe.**
-assumenosideeffects class * implements org.slf4j.Logger {
public *** trace(...);
public *** debug(...);
public *** info(...);
public *** warn(...);
public *** error(...);
}
-keep class kotlin.reflect.jvm.internal.** { *; }
-keep class com.arthenica.ffmpegkit.FFmpegKitConfig {
native <methods>;
void log(long, int, byte[]);
void statistics(long, int, float, float, long , int, double, double);
int safOpen(int);
int safClose(int);
}
-keep class com.arthenica.ffmpegkit.AbiDetect {
native <methods>;
}
-keep class com.arthenica.ffmpegkit.NativeLoader { *; }
-keep class moe.fuqiuluo.** { *; }
-keep class com.tencent.** { *; }
-keep class com.qq.** { *; }
-keep class com.google.gson.** { *; }
-keep class de.** { *; }
-keep class mqq.** { *; }
-keep class QQService.** { *; }
-keep class SummaryCard.** { *; }
-keep class tencent.** { *; }
-keepclassmembers class * {
native <methods>;
}
-keep class io.netty.** { *; }
-keep class moe.fuqiuluo.** implements com.tencent.qqnt.kernel.nativeinterface.** {
*;
}
#-renamesourcefileattribute SourceFile

View File

@ -222,7 +222,7 @@ internal object MsgSvc : BaseSvc() {
}
}
suspend fun sendMultiMsg(
suspend fun uploadMultiMsg(
uid: String,
groupUin: String?,
messages: List<PushMsgBody>,

View File

@ -1,7 +1,7 @@
package moe.fuqiuluo.qqinterface.servlet.msg
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import moe.fuqiuluo.qqinterface.servlet.msg.converter.MessageElementConverter
import moe.fuqiuluo.qqinterface.servlet.msg.converter.ElemConverter
import moe.fuqiuluo.qqinterface.servlet.msg.converter.MsgElementConverter
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
@ -21,13 +21,21 @@ internal suspend fun List<Elem>.toSegments(
1
} else if (msg.face != null) {
2
} else if (msg.lightApp != null) {
} else if (msg.notOnlineImage != null) {
4
} else if (msg.customFace != null) {
8
} else if (msg.generalFlags != null) {
37
} else if (msg.srcMsg != null) {
45
} else if (msg.lightApp != null) {
51
} else if (msg.commonElem != null) {
53
} else
throw UnsupportedOperationException("不支持的消息element类型$msg")
val converter = MessageElementConverter[elementType]
val converter = ElemConverter[elementType]
converter?.invoke(chatType, peerId, subPeer, msg)
?: throw UnsupportedOperationException("不支持的消息element类型$elementType")
}.onSuccess {

View File

@ -8,10 +8,10 @@ import moe.fuqiuluo.shamrock.tools.json
internal data class MessageSegment(
val type: String,
val data: Map<String, Any> = emptyMap()
val data: Map<String, Any?> = emptyMap()
) {
fun toJson(): JsonObject {
return hashMapOf(
return mapOf(
"type" to type.json,
"data" to data.json
).json
@ -26,7 +26,7 @@ internal fun List<MessageSegment>.toJson(): JsonArray {
internal fun List<MessageSegment>.toListMap(): List<Map<String, JsonElement>> {
return this.map {
hashMapOf(
mapOf(
"type" to it.type.json,
"data" to it.data.json
).json

View File

@ -0,0 +1,621 @@
package moe.fuqiuluo.qqinterface.servlet.msg.converter
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import io.ktor.util.*
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import kotlinx.io.core.readUInt
import moe.fuqiuluo.qqinterface.servlet.msg.MessageSegment
import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.db.ImageDB
import moe.fuqiuluo.shamrock.helper.db.ImageMapping
import moe.fuqiuluo.shamrock.helper.db.MessageDB
import moe.fuqiuluo.shamrock.utils.DeflateTools
import moe.fuqiuluo.shamrock.tools.asJsonObject
import moe.fuqiuluo.shamrock.tools.asString
import moe.fuqiuluo.shamrock.tools.toHexString
import moe.fuqiuluo.symbols.decodeProtobuf
import protobuf.message.Elem
import protobuf.message.element.commelem.ButtonExtra
import protobuf.message.element.commelem.MarkdownExtra
import protobuf.message.element.commelem.QFaceExtra
internal typealias IElemConverter = suspend (Int, String, String, Elem) -> MessageSegment
internal object ElemConverter {
private val convertMap = mapOf(
1 to ElemConverter::convertTextElem,
2 to ElemConverter::convertFaceElem,
4 to ElemConverter::convertNotOnlineImageElem,
8 to ElemConverter::convertCustomFaceElem,
// MsgConstant.KELEMTYPEPTT to ElemConverter::convertVoiceElem,
// MsgConstant.KELEMTYPEVIDEO to ElemConverter::convertVideoElem,
// MsgConstant.KELEMTYPEMARKETFACE to ElemConverter::convertMarketFaceElem,
37 to ElemConverter::convertGeneralFlagsElem,
45 to ElemConverter::convertReplyElem,
51 to ElemConverter::convertStructJsonElem,
53 to ElemConverter::convertCommonElem,
// MsgConstant.KELEMTYPEGRAYTIP to ElemConverter::convertGrayTipsElem,
// MsgConstant.KELEMTYPEFILE to ElemConverter::convertFileElem,
// //MsgConstant.KELEMTYPEMULTIFORWARD to ElemConverter::convertXmlMultiMsgElem,
// //MsgConstant.KELEMTYPESTRUCTLONGMSG to ElemConverter::convertXmlLongMsgElem,
// MsgConstant.KELEMTYPEFACEBUBBLE to ElemConverter::convertBubbleFaceElem,
)
operator fun get(type: Int): IElemConverter? = convertMap[type]
/**
* 文本 / 艾特 消息转换消息段
*/
private suspend fun convertTextElem(
chatType: Int,
peerId: String,
subPeer: String,
element: Elem
): MessageSegment {
val text = element.text!!
if (text.attr6Buf != null) {
val at = ByteReadPacket(text.attr6Buf!!)
at.discardExact(7)
val uin = at.readUInt()
return MessageSegment(
type = "at",
data = mapOf(
"qq" to uin
)
)
} else {
return MessageSegment(
type = "text",
data = mapOf(
"text" to text.str!!
)
)
}
}
/**
* 小表情 / 戳一戳 消息转换消息段
*/
private suspend fun convertFaceElem(
chatType: Int,
peerId: String,
subPeer: String,
element: Elem
): MessageSegment {
val face = element.face!!
return MessageSegment(
type = "face",
data = mapOf(
"id" to face.index!!
)
)
}
/**
* 图片消息转换消息段
*/
private suspend fun convertCustomFaceElem(
chatType: Int,
peerId: String,
subPeer: String,
element: Elem
): MessageSegment {
val customFace = element.customFace!!
val md5 = customFace.md5.toHexString()
ImageDB.getInstance().imageMappingDao().insert(
ImageMapping(md5.uppercase(), chatType, customFace.size!!.toLong())
)
val origUrl = customFace.origUrl!!
return MessageSegment(
type = "image",
data = mapOf(
"file" to md5,
"url" to when (chatType) {
MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(
origUrl,
md5
)
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(origUrl, md5)
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(origUrl, md5)
else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
},
"type" to if (customFace.origin == true) "original" else "show"
)
)
}
private suspend fun convertNotOnlineImageElem(
chatType: Int,
peerId: String,
subPeer: String,
element: Elem
): MessageSegment {
val notOnlineImage = element.notOnlineImage!!
val md5 = notOnlineImage.picMd5.toHexString()
ImageDB.getInstance().imageMappingDao().insert(
ImageMapping(md5.uppercase(), chatType, notOnlineImage.fileLen!!.toLong())
)
val origUrl = notOnlineImage.origUrl!!
return MessageSegment(
type = "image",
data = mapOf(
"file" to md5,
"url" to when (chatType) {
MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(
origUrl,
md5
)
MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(origUrl, md5)
MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(origUrl, md5)
else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
},
"type" to if (notOnlineImage.original == true) "original" else "show"
)
)
}
private suspend fun convertGeneralFlagsElem(
chatType: Int,
peerId: String,
subPeer: String,
element: Elem
): MessageSegment {
val generalFlags = element.generalFlags!!
if (generalFlags.longTextFlag == 1u) {
return MessageSegment(
type = "general_flags",
data = mapOf(
"res_id" to generalFlags.longTextResid
)
)
}
throw UnknownError("no segment")
}
//
// /**
// * 语音消息转换消息段
// */
// private suspend fun convertVoiceElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: Elem
// ): MessageSegment {
// val record = element.pttElement
//
// val md5 = if (record.fileName.startsWith("silk"))
// record.fileName.substring(5)
// else record.md5HexStr
//
// return MessageSegment(
// type = "record",
// data = mapOf(
// "file" to md5,
// "url" to when (chatType) {
// MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPttDownUrl(
// "0",
// record.md5HexStr,
// record.fileUuid
// )
//
// MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", record.fileUuid)
// MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl(
// "0",
// record.md5HexStr,
// record.fileUuid
// )
//
// else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
// }
// ).also {
// if (record.voiceChangeType != MsgConstant.KPTTVOICECHANGETYPENONE) {
// it["magic"] = "1"
// }
// if ((it["url"] as String).isBlank()) {
// it.remove("url")
// }
// }
// )
// }
//
// /**
// * 视频消息转换消息段
// */
// private suspend fun convertVideoElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: Elem
// ): MessageSegment {
// val video = element.videoElement
// val md5 = if (video.fileName.contains("/")) {
// video.videoMd5.takeIf {
// !it.isNullOrEmpty()
// }?.hex2ByteArray() ?: video.fileName.split("/").let {
// it[it.size - 2].hex2ByteArray()
// }
// } else video.fileName.split(".")[0].hex2ByteArray()
//
// //LogCenter.log({ "receive video msg: $video" }, Level.DEBUG)
//
// return MessageSegment(
// type = "video",
// data = mapOf(
// "file" to video.fileName,
// "url" to when (chatType) {
// MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
// MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CVideoDownUrl("0", md5, video.fileUuid)
// MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
// else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
// }
// ).also {
// if ((it["url"] as String).isBlank())
// it.remove("url")
// }
// )
// }
//
// /**
// * 商城大表情消息转换消息段
// */
// private suspend fun convertMarketFaceElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: Elem
// ): MessageSegment {
// val face = element.marketFaceElement
// return when (face.emojiId.lowercase()) {
// "4823d3adb15df08014ce5d6796b76ee1" -> MessageSegment("dice")
// "83c8a293ae65ca140f348120a77448ee" -> MessageSegment("rps")
// else -> MessageSegment(
// type = "mface",
// data = mapOf(
// "id" to face.emojiId
// )
// )
// }
// }
//
/**
* 回复消息转消息段
*/
private suspend fun convertReplyElem(
chatType: Int,
peerId: String,
subPeer: String,
element: Elem
): MessageSegment {
val srcMsg = element.srcMsg!!
val msgId = srcMsg.pbReserve?.msgRand?.toLong() ?: 0
val msgHash = if (msgId != 0L) {
MessageHelper.generateMsgIdHash(chatType, msgId)
} else {
val msgSeq = srcMsg.origSeqs?.first()?.toInt() ?: 0
MessageDB.getInstance().messageMappingDao()
.queryByMsgSeq(chatType, peerId, msgSeq)?.msgHashId
?: kotlin.run {
LogCenter.log("消息映射关系未找到: Message($msgSeq)", Level.WARN)
MessageHelper.generateMsgIdHash(chatType, msgId)
}
}
return MessageSegment(
type = "reply",
data = mapOf(
"id" to msgHash
)
)
}
/**
* JSON消息转消息段
*/
private suspend fun convertStructJsonElem(
chatType: Int,
peerId: String,
subPeer: String,
element: Elem
): MessageSegment {
val data = element.lightApp!!.data!!
val jsonStr =
(if (data[0].toInt() == 1) DeflateTools.uncompress(data.sliceArray(1 until data.size)) else data.sliceArray(
1 until data.size
)).toString()
val json = jsonStr.asJsonObject
return when (json["app"].asString) {
"com.tencent.multimsg" -> {
val info = json["meta"].asJsonObject["detail"].asJsonObject
MessageSegment(
type = "forward",
data = mapOf(
"id" to info["resid"].asString
)
)
}
"com.tencent.troopsharecard" -> {
val info = json["meta"].asJsonObject["contact"].asJsonObject
MessageSegment(
type = "contact",
data = mapOf(
"type" to "group",
"id" to info["jumpUrl"].asString.split("group_code=")[1]
)
)
}
"com.tencent.contact.lua" -> {
val info = json["meta"].asJsonObject["contact"].asJsonObject
MessageSegment(
type = "contact",
data = mapOf(
"type" to "private",
"id" to info["jumpUrl"].asString.split("uin=")[1]
)
)
}
"com.tencent.map" -> {
val info = json["meta"].asJsonObject["Location.Search"].asJsonObject
MessageSegment(
type = "location",
data = mapOf(
"lat" to info["lat"].asString,
"lon" to info["lng"].asString,
"content" to info["address"].asString,
"title" to info["name"].asString
)
)
}
else -> MessageSegment(
type = "json",
data = mapOf(
"data" to jsonStr
)
)
}
}
private suspend fun convertCommonElem(
chatType: Int,
peerId: String,
subPeer: String,
element: Elem
): MessageSegment {
val commonElem = element.commonElem!!
return when (commonElem.serviceType) {
37 -> {
val qFaceExtra = commonElem.elem!!.decodeProtobuf<QFaceExtra>()
when (qFaceExtra.faceId) {
358 -> MessageSegment(
type = "dice",
data = mapOf(
"result" to qFaceExtra.result!!
)
)
359 -> MessageSegment(
type = "rps",
data = mapOf(
"result" to qFaceExtra.result!!
)
)
else -> MessageSegment(
type = "face",
data = mapOf(
"id" to qFaceExtra.faceId!!,
"big" to true,
"result" to qFaceExtra.result!! // (1布 2剪 3锤) (骰子123456)
)
)
}
}
45 -> {
val markdownExtra = commonElem.elem!!.decodeProtobuf<MarkdownExtra>()
MessageSegment(
type = "markdown",
data = mapOf(
"content" to markdownExtra.content!!
)
)
}
46 -> {
val buttonExtra = commonElem.elem!!.decodeProtobuf<ButtonExtra>()
MessageSegment(
type = "button",
data = buttonExtra.field1!!.let {
mapOf(
"buttons" to it.rows!!.map { row ->
row.buttons!!.map { button ->
val renderData = button.renderData
val action = button.action
val permission = action?.permission
mapOf(
"id" to button.id,
"render_data" to mapOf(
"label" to (renderData?.label ?: ""),
"visited_label" to (renderData?.visitedLabel ?: ""),
"style" to (renderData?.style ?: 0)
),
"action" to mapOf(
"type" to (action?.type ?: 0),
"permission" to mapOf(
"type" to (permission?.type ?: 0),
"specify_role_ids" to permission?.specifyRoleIds,
"specify_user_ids" to permission?.specifyUserIds
),
"unsupport_tips" to (action?.unsupportTips ?: ""),
"data" to (action?.data ?: ""),
"reply" to action?.reply,
"enter" to action?.enter,
)
)
}
},
"appid" to it.appid
)
}
)
}
else -> MessageSegment(
type = "common",
data = mapOf(
"data" to commonElem.elem!!.encodeBase64()
)
)
}
}
//
// /**
// * 灰色提示条消息过滤
// */
// private suspend fun convertGrayTipsElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: Elem
// ): MessageSegment {
// val tip = element.grayTipElement
// when (tip.subElementType) {
// MsgConstant.GRAYTIPELEMENTSUBTYPEJSON -> {
// val notify = tip.jsonGrayTipElement
// when (notify.busiId) {
// /* 新人入群 */ 17L, /* 群戳一戳 */1061L,
// /* 群撤回 */1014L, /* 群设精消息 */2401L,
// /* 群头衔 */2407L -> {
// }
//
// else -> LogCenter.log("不支持的灰条类型(JSON): ${notify.busiId}", Level.WARN)
// }
// }
//
// MsgConstant.GRAYTIPELEMENTSUBTYPEXMLMSG -> {
// val notify = tip.xmlElement
// when (notify.busiId) {
// /* 群戳一戳 */1061L, /* 群打卡 */1068L -> {}
// else -> LogCenter.log("不支持的灰条类型(XML): ${notify.busiId}", Level.WARN)
// }
// }
//
// else -> LogCenter.log("不支持的提示类型: ${tip.subElementType}", Level.WARN)
// }
// // 提示类消息这里提供的是一个xml不具备解析通用性
// // 在这里不推送
// throw UnknownError()
// }
//
// /**
// * 文件消息转换消息段
// */
// private suspend fun convertFileElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: Elem
// ): MessageSegment {
// val fileMsg = element.fileElement
// val fileName = fileMsg.fileName
// val fileSize = fileMsg.fileSize
// val expireTime = fileMsg.expireTime ?: 0
// val fileId = fileMsg.fileUuid
// val bizId = fileMsg.fileBizId ?: 0
// val fileSubId = fileMsg.fileSubId ?: ""
// val url = when (chatType) {
// MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId)
// MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildFileDownUrl(peerId, subPeer, fileId, bizId)
// else -> RichProtoSvc.getGroupFileDownUrl(peerId.toLong(), fileId, bizId)
// }
//
// return MessageSegment(
// type = "file",
// data = mapOf(
// "name" to fileName,
// "size" to fileSize,
// "expire" to expireTime,
// "id" to fileId,
// "url" to url,
// "biz" to bizId,
// "sub" to fileSubId
// )
// )
// }
//
// /**
// * 老板QQ的合并转发信息
// */
// private suspend fun convertXmlMultiMsgElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: Elem
// ): MessageSegment {
// val multiMsg = element.multiForwardElem
// return MessageSegment(
// type = "forward",
// data = mapOf(
// "id" to multiMsg.resId
// )
// )
// }
//
// private suspend fun convertXmlLongMsgElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: Elem
// ): MessageSegment {
// val longMsg = element.structLongElem
// return MessageSegment(
// type = "forward",
// data = mapOf(
// "id" to longMsg.resId
// )
// )
// }
//
//
// private suspend fun convertBubbleFaceElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: Elem
// ): MessageSegment {
// val bubbleElement = element.faceBubbleElement
// return MessageSegment(
// type = "bubble_face",
// data = mapOf(
// "id" to bubbleElement.yellowFaceInfo.index,
// "count" to (bubbleElement.faceCount ?: 1),
// )
// )
// }
}

View File

@ -1,568 +0,0 @@
package moe.fuqiuluo.qqinterface.servlet.msg.converter
import kotlinx.io.core.ByteReadPacket
import kotlinx.io.core.discardExact
import kotlinx.io.core.readUInt
import moe.fuqiuluo.qqinterface.servlet.msg.MessageSegment
import moe.fuqiuluo.shamrock.utils.DeflateTools
import moe.fuqiuluo.shamrock.tools.asJsonObject
import moe.fuqiuluo.shamrock.tools.asString
import protobuf.message.Elem
internal typealias IMessageElementConverter = suspend (Int, String, String, Elem) -> MessageSegment
internal object MessageElementConverter {
private val convertMap = hashMapOf(
1 to MessageElementConverter::convertTextElem,
// MsgConstant.KELEMTYPEFACE to MessageElementConverter::convertFaceElem,
// MsgConstant.KELEMTYPEPIC to MessageElementConverter::convertImageElem,
// MsgConstant.KELEMTYPEPTT to MessageElementConverter::convertVoiceElem,
// MsgConstant.KELEMTYPEVIDEO to MessageElementConverter::convertVideoElem,
// MsgConstant.KELEMTYPEMARKETFACE to MessageElementConverter::convertMarketFaceElem,
51 to MessageElementConverter::convertStructJsonElem,
// MsgConstant.KELEMTYPEREPLY to MessageElementConverter::convertReplyElem,
// MsgConstant.KELEMTYPEGRAYTIP to MessageElementConverter::convertGrayTipsElem,
// MsgConstant.KELEMTYPEFILE to MessageElementConverter::convertFileElem,
// MsgConstant.KELEMTYPEMARKDOWN to MessageElementConverter::convertMarkdownElem,
// //MsgConstant.KELEMTYPEMULTIFORWARD to MessageElementConverter::convertXmlMultiMsgElem,
// //MsgConstant.KELEMTYPESTRUCTLONGMSG to MessageElementConverter::convertXmlLongMsgElem,
// MsgConstant.KELEMTYPEFACEBUBBLE to MessageElementConverter::convertBubbleFaceElem,
// MsgConstant.KELEMTYPEINLINEKEYBOARD to MessageElementConverter::convertInlineKeyboardElem,
)
operator fun get(type: Int): IMessageElementConverter? = convertMap[type]
/**
* 文本 / 艾特 消息转换消息段
*/
private suspend fun convertTextElem(
chatType: Int,
peerId: String,
subPeer: String,
element: Elem
): MessageSegment {
val text = element.text!!
if (text.attr6Buf != null) {
val at = ByteReadPacket(text.attr6Buf!!)
at.discardExact(7)
val uin = at.readUInt()
return MessageSegment(
type = "at",
data = hashMapOf(
"qq" to uin
)
)
} else {
return MessageSegment(
type = "text",
data = hashMapOf(
"text" to text.str!!
)
)
}
}
// /**
// * 小表情 / 戳一戳 消息转换消息段
// */
// private suspend fun convertFaceElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: MessageElement
// ): MessageSegment {
// val face = element.faceElement
//
// if (face.faceType == 5) {
// return MessageSegment(
// type = "poke",
// data = hashMapOf(
// "type" to face.pokeType,
// "id" to face.vaspokeId,
// "strength" to face.pokeStrength
// )
// )
// }
// when (face.faceIndex) {
// 114 -> {
// return MessageSegment(
// type = "basketball",
// data = hashMapOf(
// "id" to face.resultId.ifEmpty { "0" }.toInt(),
// )
// )
// }
//
// 358 -> {
// if (face.sourceType == 1) return MessageSegment("new_dice")
// return MessageSegment(
// type = "new_dice",
// data = hashMapOf(
// "id" to face.resultId.ifEmpty { "0" }.toInt()
// )
// )
// }
//
// 359 -> {
// if (face.resultId.isEmpty()) return MessageSegment("new_rps")
// return MessageSegment(
// type = "new_rps",
// data = hashMapOf(
// "id" to face.resultId.ifEmpty { "0" }.toInt()
// )
// )
// }
//
// 394 -> {
// //LogCenter.log(face.toString())
// return MessageSegment(
// type = "face",
// data = hashMapOf(
// "id" to face.faceIndex,
// "big" to (face.faceType == 3),
// "result" to (face.resultId ?: "1")
// )
// )
// }
//
// else -> return MessageSegment(
// type = "face",
// data = hashMapOf(
// "id" to face.faceIndex,
// "big" to (face.faceType == 3)
// )
// )
// }
// }
//
// /**
// * 图片消息转换消息段
// */
// private suspend fun convertImageElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: MessageElement
// ): MessageSegment {
// val image = element.picElement
// val md5 = image.md5HexStr ?: image.fileName
// .replace("{", "")
// .replace("}", "")
// .replace("-", "").split(".")[0]
//
// ImageDB.getInstance().imageMappingDao().insert(
// ImageMapping(md5.uppercase(), chatType, image.fileSize)
// )
//
// //LogCenter.log(image.toString())
//
// val originalUrl = image.originImageUrl ?: ""
// //LogCenter.log({ "receive image: $image" }, Level.DEBUG)
//
// return MessageSegment(
// type = "image",
// data = hashMapOf(
// "file" to md5,
// "url" to when (chatType) {
// MsgConstant.KCHATTYPEDISC, MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPicDownUrl(
// originalUrl,
// md5
// )
//
// MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPicDownUrl(originalUrl, md5)
// MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildPicDownUrl(originalUrl, md5)
// else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
// },
// "subType" to image.picSubType,
// "type" to if (image.isFlashPic == true) "flash" else if (image.original) "original" else "show"
// )
// )
// }
//
// /**
// * 语音消息转换消息段
// */
// private suspend fun convertVoiceElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: MessageElement
// ): MessageSegment {
// val record = element.pttElement
//
// val md5 = if (record.fileName.startsWith("silk"))
// record.fileName.substring(5)
// else record.md5HexStr
//
// return MessageSegment(
// type = "record",
// data = hashMapOf(
// "file" to md5,
// "url" to when (chatType) {
// MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupPttDownUrl(
// "0",
// record.md5HexStr,
// record.fileUuid
// )
//
// MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CPttDownUrl("0", record.fileUuid)
// MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupPttDownUrl(
// "0",
// record.md5HexStr,
// record.fileUuid
// )
//
// else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
// }
// ).also {
// if (record.voiceChangeType != MsgConstant.KPTTVOICECHANGETYPENONE) {
// it["magic"] = "1"
// }
// if ((it["url"] as String).isBlank()) {
// it.remove("url")
// }
// }
// )
// }
//
// /**
// * 视频消息转换消息段
// */
// private suspend fun convertVideoElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: MessageElement
// ): MessageSegment {
// val video = element.videoElement
// val md5 = if (video.fileName.contains("/")) {
// video.videoMd5.takeIf {
// !it.isNullOrEmpty()
// }?.hex2ByteArray() ?: video.fileName.split("/").let {
// it[it.size - 2].hex2ByteArray()
// }
// } else video.fileName.split(".")[0].hex2ByteArray()
//
// //LogCenter.log({ "receive video msg: $video" }, Level.DEBUG)
//
// return MessageSegment(
// type = "video",
// data = hashMapOf(
// "file" to video.fileName,
// "url" to when (chatType) {
// MsgConstant.KCHATTYPEGROUP -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
// MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CVideoDownUrl("0", md5, video.fileUuid)
// MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGroupVideoDownUrl("0", md5, video.fileUuid)
// else -> throw UnsupportedOperationException("Not supported chat type: $chatType")
// }
// ).also {
// if ((it["url"] as String).isBlank())
// it.remove("url")
// }
// )
// }
//
// /**
// * 商城大表情消息转换消息段
// */
// private suspend fun convertMarketFaceElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: MessageElement
// ): MessageSegment {
// val face = element.marketFaceElement
// return when (face.emojiId.lowercase()) {
// "4823d3adb15df08014ce5d6796b76ee1" -> MessageSegment("dice")
// "83c8a293ae65ca140f348120a77448ee" -> MessageSegment("rps")
// else -> MessageSegment(
// type = "mface",
// data = hashMapOf(
// "id" to face.emojiId
// )
// )
// }
// }
//
/**
* JSON消息转消息段
*/
private suspend fun convertStructJsonElem(
chatType: Int,
peerId: String,
subPeer: String,
element: Elem
): MessageSegment {
val data = element.lightApp!!.data!!
val jsonStr =
(if (data[0].toInt() == 1) DeflateTools.uncompress(data.sliceArray(1 until data.size)) else data.sliceArray(1 until data.size)).toString()
val json = jsonStr.asJsonObject
return when (json["app"].asString) {
"com.tencent.multimsg" -> {
val info = json["meta"].asJsonObject["detail"].asJsonObject
MessageSegment(
type = "forward",
data = mapOf(
"id" to info["resid"].asString
)
)
}
"com.tencent.troopsharecard" -> {
val info = json["meta"].asJsonObject["contact"].asJsonObject
MessageSegment(
type = "contact",
data = hashMapOf(
"type" to "group",
"id" to info["jumpUrl"].asString.split("group_code=")[1]
)
)
}
"com.tencent.contact.lua" -> {
val info = json["meta"].asJsonObject["contact"].asJsonObject
MessageSegment(
type = "contact",
data = hashMapOf(
"type" to "private",
"id" to info["jumpUrl"].asString.split("uin=")[1]
)
)
}
"com.tencent.map" -> {
val info = json["meta"].asJsonObject["Location.Search"].asJsonObject
MessageSegment(
type = "location",
data = hashMapOf(
"lat" to info["lat"].asString,
"lon" to info["lng"].asString,
"content" to info["address"].asString,
"title" to info["name"].asString
)
)
}
else -> MessageSegment(
type = "json",
data = mapOf(
"data" to jsonStr
)
)
}
}
// /**
// * 回复消息转消息段
// */
// private suspend fun convertReplyElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: MessageElement
// ): MessageSegment {
// val reply = element.replyElement
// val msgId = reply.replayMsgId
// val msgHash = if (msgId != 0L) {
// MessageHelper.generateMsgIdHash(chatType, msgId)
// } else {
// MessageDB.getInstance().messageMappingDao()
// .queryByMsgSeq(chatType, peerId, reply.replayMsgSeq?.toInt() ?: 0)?.msgHashId
// ?: kotlin.run {
// LogCenter.log("消息映射关系未找到: Message($reply)", Level.WARN)
// MessageHelper.generateMsgIdHash(chatType, reply.sourceMsgIdInRecords)
// }
// }
//
// return MessageSegment(
// type = "reply",
// data = mapOf(
// "id" to msgHash
// )
// )
// }
//
// /**
// * 灰色提示条消息过滤
// */
// private suspend fun convertGrayTipsElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: MessageElement
// ): MessageSegment {
// val tip = element.grayTipElement
// when (tip.subElementType) {
// MsgConstant.GRAYTIPELEMENTSUBTYPEJSON -> {
// val notify = tip.jsonGrayTipElement
// when (notify.busiId) {
// /* 新人入群 */ 17L, /* 群戳一戳 */1061L,
// /* 群撤回 */1014L, /* 群设精消息 */2401L,
// /* 群头衔 */2407L -> {
// }
//
// else -> LogCenter.log("不支持的灰条类型(JSON): ${notify.busiId}", Level.WARN)
// }
// }
//
// MsgConstant.GRAYTIPELEMENTSUBTYPEXMLMSG -> {
// val notify = tip.xmlElement
// when (notify.busiId) {
// /* 群戳一戳 */1061L, /* 群打卡 */1068L -> {}
// else -> LogCenter.log("不支持的灰条类型(XML): ${notify.busiId}", Level.WARN)
// }
// }
//
// else -> LogCenter.log("不支持的提示类型: ${tip.subElementType}", Level.WARN)
// }
// // 提示类消息这里提供的是一个xml不具备解析通用性
// // 在这里不推送
// throw UnknownError()
// }
//
// /**
// * 文件消息转换消息段
// */
// private suspend fun convertFileElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: MessageElement
// ): MessageSegment {
// val fileMsg = element.fileElement
// val fileName = fileMsg.fileName
// val fileSize = fileMsg.fileSize
// val expireTime = fileMsg.expireTime ?: 0
// val fileId = fileMsg.fileUuid
// val bizId = fileMsg.fileBizId ?: 0
// val fileSubId = fileMsg.fileSubId ?: ""
// val url = when (chatType) {
// MsgConstant.KCHATTYPEC2C -> RichProtoSvc.getC2CFileDownUrl(fileId, fileSubId)
// MsgConstant.KCHATTYPEGUILD -> RichProtoSvc.getGuildFileDownUrl(peerId, subPeer, fileId, bizId)
// else -> RichProtoSvc.getGroupFileDownUrl(peerId.toLong(), fileId, bizId)
// }
//
// return MessageSegment(
// type = "file",
// data = mapOf(
// "name" to fileName,
// "size" to fileSize,
// "expire" to expireTime,
// "id" to fileId,
// "url" to url,
// "biz" to bizId,
// "sub" to fileSubId
// )
// )
// }
//
// /**
// * 老板QQ的合并转发信息
// */
// private suspend fun convertXmlMultiMsgElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: MessageElement
// ): MessageSegment {
// val multiMsg = element.multiForwardMessageElement
// return MessageSegment(
// type = "forward",
// data = mapOf(
// "id" to multiMsg.resId
// )
// )
// }
//
// private suspend fun convertXmlLongMsgElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: MessageElement
// ): MessageSegment {
// val longMsg = element.structLongMessageElement
// return MessageSegment(
// type = "forward",
// data = mapOf(
// "id" to longMsg.resId
// )
// )
// }
//
// private suspend fun convertMarkdownElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: MessageElement
// ): MessageSegment {
// val markdown = element.markdownElement
// return MessageSegment(
// type = "markdown",
// data = mapOf(
// "content" to markdown.content
// )
// )
// }
//
// private suspend fun convertBubbleFaceElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: MessageElement
// ): MessageSegment {
// val bubbleElement = element.faceBubbleElement
// return MessageSegment(
// type = "bubble_face",
// data = mapOf(
// "id" to bubbleElement.yellowFaceInfo.index,
// "count" to (bubbleElement.faceCount ?: 1),
// )
// )
// }
//
// private suspend fun convertInlineKeyboardElem(
// chatType: Int,
// peerId: String,
// subPeer: String,
// element: MessageElement
// ): MessageSegment {
// val keyboard = element.inlineKeyboardElement
// return MessageSegment(
// type = "inline_keyboard",
// data = mapOf(
// "data" to buildJsonObject {
// putJsonArray("rows") {
// keyboard.rows.forEach { row ->
// add(buildJsonObject row@{
// putJsonArray("buttons") {
// row.buttons.forEach { button ->
// add(buildJsonObject {
// put("id", button.id ?: "")
// put("label", button.label ?: "")
// put("visited_label", button.visitedLabel ?: "")
// put("style", button.style)
// put("type", button.type)
// put("click_limit", button.clickLimit)
// put("unsupport_tips", button.unsupportTips ?: "")
// put("data", button.data)
// put("at_bot_show_channel_list", button.atBotShowChannelList)
// put("permission_type", button.permissionType)
// putJsonArray("specify_role_ids") {
// button.specifyRoleIds?.forEach { add(it) }
// }
// putJsonArray("specify_tinyids") {
// button.specifyTinyids?.forEach { add(it) }
// }
// })
// }
// }
// })
// }
// }
// put("bot_appid", keyboard.botAppid)
// }.toString()
// )
// )
// }
}

View File

@ -8,6 +8,9 @@ import moe.fuqiuluo.qqinterface.servlet.CardSvc
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import moe.fuqiuluo.qqinterface.servlet.ark.WeatherSvc
import moe.fuqiuluo.qqinterface.servlet.msg.toJson
import moe.fuqiuluo.qqinterface.servlet.msg.toSegments
import moe.fuqiuluo.qqinterface.servlet.transfile.*
import moe.fuqiuluo.qqinterface.servlet.transfile.PictureResource
import moe.fuqiuluo.qqinterface.servlet.transfile.Private
@ -18,6 +21,7 @@ import moe.fuqiuluo.shamrock.helper.ActionMsgException
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.LogicException
import moe.fuqiuluo.shamrock.helper.MessageHelper.messageArrayToMessageElements
import moe.fuqiuluo.shamrock.helper.ParamsException
import moe.fuqiuluo.shamrock.tools.*
import moe.fuqiuluo.shamrock.utils.DeflateTools
@ -35,35 +39,32 @@ import kotlin.random.nextULong
internal typealias IMessageElementMaker = suspend (Int, Long, String, JsonObject) -> Result<Elem>
internal object MessageElementMaker {
internal object ElemMaker {
private val makerArray = hashMapOf(
"text" to MessageElementMaker::createTextElem,
"at" to MessageElementMaker::createAtElem,
"face" to MessageElementMaker::createFaceElem,
"pic" to MessageElementMaker::createImageElem,
"image" to MessageElementMaker::createImageElem,
"text" to ElemMaker::createTextElem,
"at" to ElemMaker::createAtElem,
"face" to ElemMaker::createFaceElem,
"pic" to ElemMaker::createImageElem,
"image" to ElemMaker::createImageElem,
// "voice" to MessageElementMaker::createRecordElem,
// "record" to MessageElementMaker::createRecordElem,
// "video" to MessageElementMaker::createVideoElem,
"markdown" to MessageElementMaker::createMarkdownElem,
"button" to MessageElementMaker::createButtonElem,
"inline_keyboard" to MessageElementMaker::createButtonElem,
// "dice" to MessageElementMaker::createDiceElem,
// "rps" to MessageElementMaker::createRpsElem,
"basketball" to MessageElementMaker::createBasketballElem,
"new_dice" to MessageElementMaker::createNewDiceElem,
"new_rps" to MessageElementMaker::createNewRpsElem,
"poke" to MessageElementMaker::createPokeElem,
"markdown" to ElemMaker::createMarkdownElem,
"button" to ElemMaker::createButtonElem,
"inline_keyboard" to ElemMaker::createButtonElem,
"dice" to ElemMaker::createNewDiceElem,
"rps" to ElemMaker::createNewRpsElem,
"poke" to ElemMaker::createPokeElem,
// "anonymous" to MessageElementMaker::createAnonymousElem,
// "share" to MessageElementMaker::createShareElem,
// "contact" to MessageElementMaker::createContactElem,
// "location" to MessageElementMaker::createLocationElem,
// "music" to MessageElementMaker::createMusicElem,
"reply" to MessageElementMaker::createReplyElem,
"reply" to ElemMaker::createReplyElem,
// "touch" to MessageElementMaker::createTouchElem,
// "weather" to MessageElementMaker::createWeatherElem,
"json" to MessageElementMaker::createJsonElem,
//"node" to MessageMaker::createNodeElem,
"weather" to ElemMaker::createWeatherElem,
"json" to ElemMaker::createJsonElem,
// "node" to MessageMaker::createNodeElem,
//"multi_msg" to MessageMaker::createLongMsgStruct,
//"bubble_face" to MessageElementMaker::createBubbleFaceElem,
)
@ -111,7 +112,11 @@ internal object MessageElementMaker {
else -> {
qq = qqStr.toLong()
type = 0
"@" + (data["name"].asStringOrNull ?: GroupSvc.getTroopMemberInfoByUinV2(peerId.toLong(), qq, true)
"@" + (data["name"].asStringOrNull ?: GroupSvc.getTroopMemberInfoByUinV2(
peerId.toLong(),
qq,
true
)
.let {
val info = it.getOrNull()
if (info == null)
@ -164,9 +169,31 @@ internal object MessageElementMaker {
data: JsonObject
): Result<Elem> {
data.checkAndThrow("id")
val elem = Elem(
face = FaceMsg(data["id"].asInt)
)
val faceId = data["id"].asInt
val elem = if (data["big"].asBooleanOrNull == true) {
Elem(
commonElem = CommonElem(
serviceType = 37,
elem = QFaceExtra(
packId = "1",
stickerId = "1",
faceId = faceId,
field4 = 1,
field5 = 1,
result = "",
faceText = "", //todo 表情名字
field9 = 1
).toByteArray(),
businessType = 1
)
)
} else {
Elem(
face = FaceMsg(
index = faceId
)
)
}
return Result.success(elem)
}
@ -259,7 +286,7 @@ internal object MessageElementMaker {
width = picWidth.toUInt(),
height = picHeight.toUInt(),
size = QQNTWrapperUtil.CppProxy.getFileSize(file.absolutePath).toUInt(),
origin = if (isOriginal) 1u else 0u,
origin = isOriginal,
thumbWidth = 0u,
thumbHeight = 0u,
pbReserve = CustomFace.Companion.PbReserve(field1 = 0)
@ -279,7 +306,7 @@ internal object MessageElementMaker {
picHeight = picWidth.toUInt(),
picWidth = picHeight.toUInt(),
resId = "".toByteArray(),
original = if (isOriginal) 1u else 0u, // true
original = isOriginal, // true
pbReserve = NotOnlineImage.Companion.PbReserve(field1 = 0)
)
)
@ -324,7 +351,7 @@ internal object MessageElementMaker {
),
type = 0u,
pbReserve = SourceMsg.Companion.PbReserve(
field3 = Random.nextULong(),
msgRand = Random.nextInt().toULong(),
field8 = Random.nextInt(0, 10000)
),
)
@ -340,10 +367,19 @@ internal object MessageElementMaker {
senderUin = msg.senderUin.toULong(),
time = msg.msgTime.toULong(),
flag = 1u,
// elems = msg.elements.toSegments(),
elems = messageArrayToMessageElements(
msg.chatType,
msg.msgId,
msg.peerUin.toString(),
msg.elements.toSegments(
msg.chatType,
if (msg.chatType == MsgConstant.KCHATTYPEGUILD) msg.guildId else msg.peerUin.toString(),
msg.channelId ?: msg.peerUin.toString()
).toJson()
).second,
type = 0u,
pbReserve = SourceMsg.Companion.PbReserve(
field3 = Random.nextULong(),
msgRand = Random.nextULong(),
senderUid = msg.senderUid,
receiverUid = TicketSvc.getUid(),
field8 = Random.nextInt(0, 10000)
@ -370,6 +406,38 @@ internal object MessageElementMaker {
return Result.success(elem)
}
private suspend fun createWeatherElem(
chatType: Int,
msgId: Long,
peerId: String,
data: JsonObject
): Result<Elem> {
var code = data["code"].asIntOrNull
if (code == null) {
data.checkAndThrow("city")
val city = data["city"].asString
code = WeatherSvc.searchCity(city).onFailure {
LogCenter.log("无法获取城市天气: $city", Level.ERROR)
}.getOrNull()?.firstOrNull()?.adcode
}
if (code != null) {
WeatherSvc.fetchWeatherCard(code).onSuccess {
// OidbSvc.0xdc2_34
// 00 00 00 DF 08 C2 1B 10 22 22 C4 01 0A B7 01 08 A2 E0 F2 2F 10 01 18 00 2A 02 08 01 58 FB 91 F6 AE 02 62 A1 01 08 01 52 08 E5 8C 97 E4 BA AC 20 20 5A 19 2D 33 C2 B0 2F 33 C2 B0 0A E7 A9 BA E6 B0 94 E8 B4 A8 E9 87 8F 3A E8 89 AF 62 11 5B E5 88 86 E4 BA AB 5D 20 E5 8C 97 E4 BA AC 20 20 6A 25 68 74 74 70 73 3A 2F 2F 77 65 61 74 68 65 72 2E 6D 70 2E 71 71 2E 63 6F 6D 2F 3F 73 74 3D 30 26 5F 77 76 3D 31 72 3E 68 74 74 70 73 3A 2F 2F 69 6D 67 63 61 63 68 65 2E 71 71 2E 63 6F 6D 2F 61 63 2F 71 71 77 65 61 74 68 65 72 2F 69 6D 61 67 65 2F 73 68 61 72 65 5F 69 63 6F 6E 2F 66 69 6E 65 2E 70 6E 67 12 08 08 01 10 FB 91 F6 AE 02 32 0D 61 6E 64 72 6F 69 64 20 39 2E 30 2E 38
return createJsonElem(
chatType, msgId, peerId, it["weekStore"]
.asJsonObject["share"].asJsonObject
)
}.onFailure {
LogCenter.log("无法发送天气分享", Level.ERROR)
}
}
return Result.failure(ActionMsgException)
}
private suspend fun createPokeElem(
chatType: Int,
msgId: Long,
@ -391,31 +459,6 @@ internal object MessageElementMaker {
return Result.success(elem)
}
private suspend fun createBasketballElem(
chatType: Int,
msgId: Long,
peerId: String,
data: JsonObject
): Result<Elem> {
val elem = Elem(
commonElem = CommonElem(
serviceType = 37,
elem = QFaceExtra(
packId = "1",
stickerId = "13",
faceId = 114,
field4 = 1,
field5 = 2,
field6 = "",
faceText = "/篮球",
field9 = 1
).toByteArray(),
businessType = 2
)
)
return Result.success(elem)
}
private suspend fun createNewDiceElem(
chatType: Int,
msgId: Long,
@ -431,7 +474,7 @@ internal object MessageElementMaker {
faceId = 358,
field4 = 1,
field5 = 2,
field6 = "",
result = "",
faceText = "/骰子",
field9 = 1
).toByteArray(),
@ -456,7 +499,7 @@ internal object MessageElementMaker {
faceId = 359,
field4 = 1,
field5 = 2,
field6 = "",
result = "",
faceText = "/包剪锤",
field9 = 1
).toByteArray(),
@ -489,19 +532,20 @@ internal object MessageElementMaker {
peerId: String,
data: JsonObject
): Result<Elem> {
data.checkAndThrow("rows")
data.checkAndThrow("buttons")
val elem = Elem(
commonElem = CommonElem(
serviceType = 46,
elem = ButtonExtra(
field1 = Object1(
rows = data["rows"].asJsonArray.map { row ->
rows = data["buttons"].asJsonArray.map { row ->
Row(buttons = row.asJsonArray.map {
val button = it.asJsonObject
val renderData = button["render_data"].asJsonObject
val action = button["action"].asJsonObject
val permission = action["permission"].asJsonObject
Button(
id = button["id"].asIntOrNull,
id = button["id"].asStringOrNull,
renderData = RenderData(
label = renderData["label"].asString,
visitedLabel = renderData["visited_label"].asString,
@ -510,9 +554,9 @@ internal object MessageElementMaker {
action = Action(
type = action["type"].asInt,
permission = Permission(
type = action["permission"].asJsonObject["type"].asInt,
specifyRoleIds = action["permission"].asJsonObject["specify_role_ids"].asJsonArrayOrNull?.map { id -> id.asString },
specifyUserIds = action["permission"].asJsonObject["specify_user_ids"].asJsonArrayOrNull?.map { id -> id.asString }
type = permission["type"].asInt,
specifyRoleIds = permission["specify_role_ids"].asJsonArrayOrNull?.map { id -> id.asString },
specifyUserIds = permission["specify_user_ids"].asJsonArrayOrNull?.map { id -> id.asString }
),
unsupportTips = action["unsupport_tips"].asString,
data = action["data"].asString,

View File

@ -3,6 +3,7 @@ package moe.fuqiuluo.qqinterface.servlet.msg.maker
import android.graphics.BitmapFactory
import androidx.exifinterface.media.ExifInterface
import com.tencent.mobileqq.app.QQAppInterface
import com.tencent.mobileqq.data.MessageForPic
import com.tencent.mobileqq.emoticon.QQSysFaceUtil
import com.tencent.mobileqq.pb.ByteStringMicro
import com.tencent.mobileqq.qroute.QRoute
@ -57,37 +58,37 @@ import kotlin.random.nextInt
internal typealias IMsgElementMaker = suspend (Int, Long, String, JsonObject) -> Result<MsgElement>
internal object MsgElementMaker {
internal object NtMsgElementMaker {
private val makerMap = hashMapOf(
"text" to MsgElementMaker::createTextElem,
"face" to MsgElementMaker::createFaceElem,
"pic" to MsgElementMaker::createImageElem,
"image" to MsgElementMaker::createImageElem,
"voice" to MsgElementMaker::createRecordElem,
"record" to MsgElementMaker::createRecordElem,
"at" to MsgElementMaker::createAtElem,
"video" to MsgElementMaker::createVideoElem,
"markdown" to MsgElementMaker::createMarkdownElem,
"dice" to MsgElementMaker::createDiceElem,
"rps" to MsgElementMaker::createRpsElem,
"poke" to MsgElementMaker::createPokeElem,
"anonymous" to MsgElementMaker::createAnonymousElem,
"share" to MsgElementMaker::createShareElem,
"contact" to MsgElementMaker::createContactElem,
"location" to MsgElementMaker::createLocationElem,
"music" to MsgElementMaker::createMusicElem,
"reply" to MsgElementMaker::createReplyElem,
"touch" to MsgElementMaker::createTouchElem,
"weather" to MsgElementMaker::createWeatherElem,
"json" to MsgElementMaker::createJsonElem,
"new_dice" to MsgElementMaker::createNewDiceElem,
"new_rps" to MsgElementMaker::createNewRpsElem,
"basketball" to MsgElementMaker::createBasketballElem,
"text" to NtMsgElementMaker::createTextElem,
"face" to NtMsgElementMaker::createFaceElem,
"pic" to NtMsgElementMaker::createImageElem,
"image" to NtMsgElementMaker::createImageElem,
"voice" to NtMsgElementMaker::createRecordElem,
"record" to NtMsgElementMaker::createRecordElem,
"at" to NtMsgElementMaker::createAtElem,
"video" to NtMsgElementMaker::createVideoElem,
"markdown" to NtMsgElementMaker::createMarkdownElem,
"dice" to NtMsgElementMaker::createDiceElem,
"rps" to NtMsgElementMaker::createRpsElem,
"poke" to NtMsgElementMaker::createPokeElem,
"anonymous" to NtMsgElementMaker::createAnonymousElem,
"share" to NtMsgElementMaker::createShareElem,
"contact" to NtMsgElementMaker::createContactElem,
"location" to NtMsgElementMaker::createLocationElem,
"music" to NtMsgElementMaker::createMusicElem,
"reply" to NtMsgElementMaker::createReplyElem,
"touch" to NtMsgElementMaker::createTouchElem,
"weather" to NtMsgElementMaker::createWeatherElem,
"json" to NtMsgElementMaker::createJsonElem,
"new_dice" to NtMsgElementMaker::createNewDiceElem,
"new_rps" to NtMsgElementMaker::createNewRpsElem,
"basketball" to NtMsgElementMaker::createBasketballElem,
//"node" to MessageMaker::createNodeElem,
//"multi_msg" to MessageMaker::createLongMsgStruct,
"bubble_face" to MsgElementMaker::createBubbleFaceElem,
"button" to MsgElementMaker::createInlineKeywordElem,
"inline_keyboard" to MsgElementMaker::createInlineKeywordElem
"bubble_face" to NtMsgElementMaker::createBubbleFaceElem,
"button" to NtMsgElementMaker::createInlineKeywordElem,
"inline_keyboard" to NtMsgElementMaker::createInlineKeywordElem
)
operator fun get(type: String): IMsgElementMaker? = makerMap[type]
@ -332,7 +333,6 @@ internal object MsgElementMaker {
LogCenter.log("无法发送天气分享", Level.ERROR)
}
}
return Result.failure(ActionMsgException)
}
@ -1006,6 +1006,10 @@ internal object MsgElementMaker {
pic.picSubType = data["subType"].asIntOrNull ?: 0
pic.isFlashPic = isFlash
//if (PlatformUtils.getQQVersionCode() >= PlatformUtils.QQ_9_0_8_VER && !ShamrockConfig.enableOldBDH()) {
// pic.storeID = 1
//}
elem.picElement = pic
return Result.success(elem)

View File

@ -1,5 +1,7 @@
package moe.fuqiuluo.qqinterface.servlet.transfile
import com.tencent.mobileqq.data.MessageRecord
internal enum class ContactType {
TROOP,
PRIVATE,
@ -8,12 +10,20 @@ internal enum class ContactType {
internal interface TransTarget {
val id: String
val type: ContactType
val mRec: MessageRecord?
}
internal class Troop(override val id: String): TransTarget {
internal class Troop(
override val id: String,
override val mRec: MessageRecord? = null
): TransTarget {
override val type: ContactType = ContactType.TROOP
}
internal class Private(override val id: String): TransTarget {
internal class Private(
override val id: String,
override val mRec: MessageRecord? = null
): TransTarget {
override val type: ContactType = ContactType.PRIVATE
}

View File

@ -2,15 +2,18 @@
package moe.fuqiuluo.qqinterface.servlet.transfile
import com.tencent.mobileqq.transfile.BaseTransProcessor
import com.tencent.mobileqq.transfile.FileMsg
import com.tencent.mobileqq.transfile.TransferRequest
import com.tencent.mobileqq.transfile.api.ITransFileController
import com.tencent.mobileqq.utils.httputils.IHttpCommunicatorListener
import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.utils.MD5
import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
import mqq.app.AppRuntime
@ -81,6 +84,7 @@ internal abstract class FileTransfer {
}
suspendCancellableCoroutine { continuation ->
GlobalScope.launch {
lateinit var processor: IHttpCommunicatorListener
while (
//service.findProcessor(
// transferRequest.keyForTransfer // uin + uniseq
@ -89,8 +93,13 @@ internal abstract class FileTransfer {
// 如果上传处理器依旧存在,说明没有上传成功
&& service.isWorking.get()
) {
processor = service.findProcessor(runtime.currentAccountUin, transferRequest.mUniseq)
delay(100)
}
if (processor is BaseTransProcessor && processor.file != null) {
val fileMsg = processor.file
LogCenter.log("[OldBDH] 资源上传结束(fileId = ${fileMsg.fileID}, fileKey = ${fileMsg.fileKey}, path = ${fileMsg.filePath})")
}
continuation.resume(true)
}
// 实现取消上传器

View File

@ -0,0 +1,227 @@
package moe.fuqiuluo.qqinterface.servlet.transfile
import kotlinx.atomicfu.atomic
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
import moe.fuqiuluo.qqinterface.servlet.BaseSvc
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.tools.hex2ByteArray
import moe.fuqiuluo.shamrock.tools.slice
import moe.fuqiuluo.symbols.decodeProtobuf
import protobuf.auto.toByteArray
import protobuf.oidb.TrpcOidb
import protobuf.oidb.cmd0x11c5.ClientMeta
import protobuf.oidb.cmd0x11c5.CodecConfigReq
import protobuf.oidb.cmd0x11c5.CommonHead
import protobuf.oidb.cmd0x11c5.DownloadExt
import protobuf.oidb.cmd0x11c5.DownloadReq
import protobuf.oidb.cmd0x11c5.FileInfo
import protobuf.oidb.cmd0x11c5.FileType
import protobuf.oidb.cmd0x11c5.IndexNode
import protobuf.oidb.cmd0x11c5.MultiMediaReqHead
import protobuf.oidb.cmd0x11c5.NtV2RichMediaReq
import protobuf.oidb.cmd0x11c5.NtV2RichMediaRsp
import protobuf.oidb.cmd0x11c5.SceneInfo
import protobuf.oidb.cmd0x11c5.UploadInfo
import protobuf.oidb.cmd0x11c5.UploadReq
import protobuf.oidb.cmd0x11c5.VideoDownloadExt
import protobuf.oidb.cmd0x388.Cmd0x388ReqBody
import protobuf.oidb.cmd0x388.Cmd0x388RspBody
import protobuf.oidb.cmd0x388.TryUpImgReq
import java.io.File
import kotlin.random.Random
import kotlin.random.nextUInt
import kotlin.random.nextULong
internal object NtV2RichMediaSvc: BaseSvc() {
private val requestIdSeq = atomic(2L)
/**
* 获取NT图片的RKEY
*/
suspend fun getNtPicRKey(
fileId: String,
md5: String,
sha: String,
fileSize: ULong,
width: UInt,
height: UInt,
sceneBuilder: suspend SceneInfo.() -> Unit
): Result<String> {
runCatching {
val req = NtV2RichMediaReq(
head = MultiMediaReqHead(
commonHead = CommonHead(
requestId = requestIdSeq.incrementAndGet().toULong(),
cmd = 200u
),
sceneInfo = SceneInfo(
requestType = 2u,
businessType = 1u,
).apply {
sceneBuilder()
},
clientMeta = ClientMeta(2u)
),
download = DownloadReq(
IndexNode(
FileInfo(
fileSize = fileSize,
md5 = md5.lowercase(),
sha1 = sha.lowercase(),
name = "${md5}.jpg",
fileType = FileType(
fileType = 1u,
picFormat = 1000u,
videoFormat = 0u,
voiceFormat = 0u
),
width = width,
height = height,
time = 0u,
original = 1u
),
fileUuid = fileId,
storeId = 1u,
uploadTime = 0u,
ttl = 0u,
subType = 0u,
storeAppId = 0u
),
DownloadExt(
video = VideoDownloadExt(
busiType = 0u,
subBusiType = 0u,
msgCodecConfig = CodecConfigReq(
platformChipinfo = "",
osVer = "",
deviceName = ""
),
flag = 1u
)
)
)
).toByteArray()
val buffer = sendOidbAW("OidbSvcTrpcTcp.0x11c5_200", 4549, 200, req, true)?.slice(4)
buffer?.decodeProtobuf<TrpcOidb>()?.buffer?.decodeProtobuf<NtV2RichMediaRsp>()?.download?.rkeyParam?.let {
return Result.success(it)
}
}.onFailure {
return Result.failure(it)
}
return Result.failure(Exception("unable to get c2c nt pic"))
}
/**
* 请求上传Nt图片
*/
suspend fun requestUploadNtPic(
file: File,
md5: String,
sha: String,
name: String,
width: UInt,
height: UInt,
sceneBuilder: suspend SceneInfo.() -> Unit
) {
val req = NtV2RichMediaReq(
head = MultiMediaReqHead(
commonHead = CommonHead(
requestId = requestIdSeq.incrementAndGet().toULong(),
cmd = 100u
),
sceneInfo = SceneInfo(
requestType = 2u,
businessType = 1u,
).apply {
sceneBuilder()
},
clientMeta = ClientMeta(2u)
),
upload = UploadReq(
listOf(UploadInfo(
FileInfo(
fileSize = file.length().toULong(),
md5 = md5,
sha1 = sha,
name = name,
fileType = FileType(
fileType = 1u,
picFormat = 1000u,
videoFormat = 0u,
voiceFormat = 0u
),
width = width,
height = height,
time = 0u,
original = 1u
),
subFileType = 0u
)),
tryFastUploadCompleted = true,
srvSendMsg = false,
clientRandomId = Random.nextULong(),
compatQMsgSceneType = 1u,
clientSeq = Random.nextUInt(),
noNeedCompatMsg = true
)
).toByteArray()
val buffer = sendOidbAW("OidbSvcTrpcTcp.0x11c5_100", 4549, 100, req, true)?.slice(4)
val rsp = buffer?.decodeProtobuf<TrpcOidb>()?.buffer?.decodeProtobuf<NtV2RichMediaRsp>()
LogCenter.log("requestUploadPic => rsp: $rsp")
}
suspend fun requestUploadGroupPic(
groupId: ULong,
md5: String,
fileSize: ULong,
width: UInt,
height: UInt,
): Result<TryUpPicData> {
return runCatching {
val rspBuffer = sendBufferAW("ImgStore.GroupPicUp", true, Cmd0x388ReqBody(
netType = 3,
subCmd = 1,
msgTryUpImg = arrayListOf(
TryUpImgReq(
groupCode = groupId.toLong(),
srcUin = TicketSvc.getLongUin(),
fileMd5 = md5.hex2ByteArray(),
fileSize = fileSize.toLong(),
fileName = "$md5.jpg",
srcTerm = 2,
platformType = 9,
buType = 212,
picWidth = width.toInt(),
picHeight = height.toInt(),
picType = 1000,
buildVer = "1.0.0",
originalPic = 1,
fileIndex = byteArrayOf(),
srvUpload = 0
)
),
).toByteArray())!!
val rsp = rspBuffer.decodeProtobuf<Cmd0x388RspBody>()
.msgTryUpImgRsp!!.first()
TryUpPicData(
uKey = rsp.ukey,
exist = rsp.fileExist,
fileId = rsp.fileId.toULong(),
upIp = rsp.upIp,
upPort = rsp.upPort
)
}
}
@Serializable
data class TryUpPicData(
@SerialName("ukey") val uKey: ByteArray,
@SerialName("exist") val exist: Boolean,
@SerialName("file_id") val fileId: ULong,
@SerialName("up_ip") var upIp: ArrayList<Long>? = null,
@SerialName("up_port") var upPort: ArrayList<Int>? = null,
)
}

View File

@ -10,6 +10,7 @@ import kotlinx.atomicfu.atomic
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.serialization.ExperimentalSerializationApi
import moe.fuqiuluo.qqinterface.servlet.BaseSvc
import moe.fuqiuluo.qqinterface.servlet.transfile.NtV2RichMediaSvc.getNtPicRKey
import moe.fuqiuluo.shamrock.helper.ContactHelper
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
@ -53,8 +54,6 @@ private const val MULTIMEDIA_DOMAIN = "multimedia.nt.qq.com.cn"
private const val C2C_PIC = "c2cpicdw.qpic.cn"
internal object RichProtoSvc: BaseSvc() {
private val requestId = atomic(2L)
suspend fun getGuildFileDownUrl(peerId: String, channelId: String, fileId: String, bizId: Int): String {
val buffer = sendOidbAW("OidbSvcTrpcTcp.0xfc2_0", 4034, 0, Oidb0xfc2ReqBody(
msgCmd = 1200,
@ -105,7 +104,7 @@ internal object RichProtoSvc: BaseSvc() {
val domain = if (!result.download_file_rsp.str_download_dns.has())
("https://" + result.download_file_rsp.str_download_ip.get())
else ("http://" + result.download_file_rsp.str_download_dns.get())
else ("http://" + result.download_file_rsp.str_download_dns.get().toByteArray().decodeToString())
val downloadUrl = result.download_file_rsp.bytes_download_url.get().toByteArray().toHexString()
val appId = MobileQQ.getMobileQQ().appId
val version = PlatformUtils.getQQVersion(MobileQQ.getContext())
@ -279,81 +278,6 @@ internal object RichProtoSvc: BaseSvc() {
return "https://$domain/qmeetpic/0/0-0-${md5.uppercase()}/0?term=2"
}
suspend fun getNtPicRKey(
fileId: String,
md5: String,
sha: String,
fileSize: ULong,
width: UInt,
height: UInt,
sceneBuilder: suspend SceneInfo.() -> Unit
): Result<String> {
runCatching {
val req = run {
NtV2RichMediaReq(
head = MultiMediaReqHead(
commonHead = CommonHead(
requestId = requestId.incrementAndGet().toULong(),
cmd = 200u
),
sceneInfo = SceneInfo(
requestType = 2u,
businessType = 1u,
).apply {
sceneBuilder()
},
clientMeta = ClientMeta(2u)
),
download = DownloadReq(
IndexNode(
FileInfo(
fileSize = fileSize,
md5 = md5.lowercase(),
sha1 = sha.lowercase(),
name = "${md5}.jpg",
fileType = FileType(
fileType = 1u,
picFormat = 1000u,
videoFormat = 0u,
voiceFormat = 0u
),
width = width,
height = height,
time = 0u,
original = 1u
),
fileUuid = fileId,
storeId = 1u,
uploadTime = 0u,
ttl = 0u,
subType = 0u,
storeAppId = 0u
),
DownloadExt(
video = VideoDownloadExt(
busiType = 0u,
subBusiType = 0u,
msgCodecConfig = CodecConfigReq(
platformChipinfo = "",
osVer = "",
deviceName = ""
),
flag = 1u
)
)
)
)
}.toByteArray()
val buffer = sendOidbAW("OidbSvcTrpcTcp.0x11c5_200", 4549, 200, req, true)?.slice(4)
buffer?.decodeProtobuf<TrpcOidb>()?.buffer?.decodeProtobuf<NtV2RichMediaRsp>()?.download?.rkeyParam?.let {
return Result.success(it)
}
}.onFailure {
return Result.failure(it)
}
return Result.failure(Exception("unable to get c2c nt pic"))
}
suspend fun getC2CVideoDownUrl(
peerId: String,
md5: ByteArray,

View File

@ -1,6 +1,8 @@
package moe.fuqiuluo.qqinterface.servlet.transfile
import com.tencent.mobileqq.data.MessageForPic
import com.tencent.mobileqq.data.MessageForShortVideo
import com.tencent.mobileqq.data.MessageRecord
import com.tencent.mobileqq.transfile.FileMsg
import com.tencent.mobileqq.transfile.TransferRequest
import moe.fuqiuluo.shamrock.utils.MD5
@ -11,16 +13,15 @@ import moe.fuqiuluo.shamrock.helper.TransfileHelper
internal object Transfer: FileTransfer() {
private val ROUTE = mapOf<ContactType, Map<ResourceType, suspend TransTarget.(Resource) -> Boolean>>(
ContactType.TROOP to mapOf(
Picture to { uploadGroupPic(id, (it as PictureResource).src) },
Picture to { uploadGroupPic(id, (it as PictureResource).src, mRec) },
Voice to { uploadGroupVoice(id, (it as VoiceResource).src) },
Video to { uploadGroupVideo(id, (it as VideoResource).src, it.thumb) },
),
ContactType.PRIVATE to mapOf(
Picture to { uploadC2CPic(id, (it as PictureResource).src) },
Picture to { uploadC2CPic(id, (it as PictureResource).src, mRec) },
Voice to { uploadC2CVoice(id, (it as VoiceResource).src) },
Video to { uploadC2CVideo(id, (it as VideoResource).src, it.thumb) },
)
)
@ -83,6 +84,7 @@ internal object Transfer: FileTransfer() {
suspend fun uploadC2CPic(
peerId: String,
file: File,
record: MessageRecord? = null,
wait: Boolean = true
): Boolean {
return transC2CResource(peerId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_CAMERA, wait) {
@ -93,22 +95,24 @@ internal object Transfer: FileTransfer() {
it.mExtraObj = picUpExtraInfo
it.mIsPresend = true
it.delayShowProgressTimeInMs = 2000
it.mRec = record
}
}
suspend fun uploadGroupPic(
groupId: String,
file: File,
record: MessageRecord? = null,
wait: Boolean = true
): Boolean {
return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_CAMERA, wait) {
val picUpExtraInfo = TransferRequest.PicUpExtraInfo()
//picUpExtraInfo.mIsRaw = !TransfileHelper.isGifFile(file)
picUpExtraInfo.mIsRaw = false
picUpExtraInfo.mUinType = FileMsg.UIN_TROOP
it.mPicSendSource = 8
it.delayShowProgressTimeInMs = 2000
it.mExtraObj = picUpExtraInfo
it.mRec = record
}
}

View File

@ -16,8 +16,8 @@ import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.jsonObject
import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.qqinterface.servlet.msg.maker.MessageElementMaker
import moe.fuqiuluo.qqinterface.servlet.msg.maker.MsgElementMaker
import moe.fuqiuluo.qqinterface.servlet.msg.maker.ElemMaker
import moe.fuqiuluo.qqinterface.servlet.msg.maker.NtMsgElementMaker
import moe.fuqiuluo.shamrock.helper.db.MessageDB
import moe.fuqiuluo.shamrock.helper.db.MessageMapping
import moe.fuqiuluo.shamrock.remote.structures.SendMsgResult
@ -29,6 +29,7 @@ import moe.fuqiuluo.shamrock.tools.jsonArray
import protobuf.message.Elem
import kotlin.coroutines.resume
import kotlin.math.abs
import kotlin.time.Duration.Companion.seconds
internal object MessageHelper {
suspend fun sendMessageWithoutMsgId(
@ -66,12 +67,15 @@ internal object MessageHelper {
suspend fun resendMsg(contact: Contact, msgId: Long, retryCnt: Int, msgHashId: Int): Result<SendMsgResult> {
if (retryCnt < 0) return Result.failure(SendMsgException("消息发送超时次数过多"))
val service = QRoute.api(IMsgService::class.java)
val result = withTimeoutOrNull(15000) {
if (suspendCancellableCoroutine {
service.resendMsg(contact, msgId) { result, _ ->
it.resume(result)
}
} != 0) {
val result = withTimeoutOrNull(15.seconds) {
val resendRet = suspendCancellableCoroutine {
service.resendMsg(contact, msgId) { result, _ ->
it.resume(result)
}
}
if (resendRet != 0 &&
resendRet != 4 // 使用OldBDH 100%触发
) {
resendMsg(contact, msgId, retryCnt - 1, msgHashId)
} else {
Result.success(SendMsgResult(msgHashId, msgId, System.currentTimeMillis()))
@ -287,18 +291,18 @@ internal object MessageHelper {
suspend fun messageArrayToMsgElements(
chatType: Int,
msgId: Long,
targetUin: String,
peerId: String,
messageList: JsonArray
): Pair<Boolean, ArrayList<MsgElement>> {
val msgList = arrayListOf<MsgElement>()
var hasActionMsg = false
messageList.forEach {
val msg = it.jsonObject
val maker = MsgElementMaker[msg["type"].asString]
val maker = NtMsgElementMaker[msg["type"].asString]
if (maker != null) {
try {
val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject
maker(chatType, msgId, targetUin, data).onSuccess { msgElem ->
maker(chatType, msgId, peerId, data).onSuccess { msgElem ->
msgList.add(msgElem)
}.onFailure {
if (it.javaClass != ActionMsgException::class.java) {
@ -321,18 +325,18 @@ internal object MessageHelper {
suspend fun messageArrayToMessageElements(
chatType: Int,
msgId: Long,
targetUin: String,
peerId: String,
messageList: JsonArray
): Pair<Boolean, ArrayList<Elem>> {
val msgList = arrayListOf<Elem>()
var hasActionMsg = false
messageList.forEach {
val msg = it.jsonObject
val maker = MessageElementMaker[msg["type"].asString]
val maker = ElemMaker[msg["type"].asString]
if (maker != null) {
try {
val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject
maker(chatType, msgId, targetUin, data).onSuccess { msgElem ->
maker(chatType, msgId, peerId, data).onSuccess { msgElem ->
msgList.add(msgElem)
}.onFailure {
if (it.javaClass != ActionMsgException::class.java) {
@ -427,11 +431,11 @@ internal object MessageHelper {
params[key] = value.json
}
}
val data = hashMapOf(
val data = mapOf(
"type" to it["_type"]!!.json,
"data" to JsonObject(params)
)
arrayList.add(JsonObject(data))
arrayList.add(data.json)
}
return arrayList.jsonArray
}

View File

@ -8,17 +8,17 @@ import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.symbols.OneBotHandler
@OneBotHandler("get_group_root_files")
internal object GetGroupRootFiles: IActionHandler() {
internal object GetGroupRootFiles : IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val groupId = session.getLong("group_id")
return invoke(groupId, session.echo)
}
suspend operator fun invoke(groupId: Long, echo: JsonElement = EmptyJsonString): String {
FileSvc.getGroupRootFiles(groupId).onSuccess {
return ok(it, echo = echo)
}.getOrNull()
return error(why = "获取失败", echo = echo)
return ok(
FileSvc.getGroupRootFiles(groupId).getOrElse { return error(why = "获取失败: $it", echo = echo) },
echo = echo
)
}
override val requiredParams: Array<String> = arrayOf("group_id")

View File

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

View File

@ -0,0 +1,29 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import moe.fuqiuluo.qqinterface.servlet.transfile.NtV2RichMediaSvc
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.symbols.OneBotHandler
@OneBotHandler("request_upload_group_image")
internal object RequestUploadGroupImage: IActionHandler() {
override suspend fun internalHandle(session: ActionSession): String {
val md5 = session.getString("md5").uppercase()
val fileSize = session.getLong("file_size")
val width = session.getInt("width")
val height = session.getInt("height")
val groupId = session.getString("group_id")
NtV2RichMediaSvc.requestUploadGroupPic(
groupId.toULong(),
md5,
fileSize.toULong(),
width.toUInt(),
height.toUInt()
).onSuccess {
return ok(it, session.echo)
}.onFailure {
return error(it.message ?: it.toString(), session.echo)
}
return logic("request_upload_group_image failed", session.echo)
}
}

View File

@ -80,19 +80,18 @@ internal object SendForwardMessage : IActionHandler() {
fromId: String = peerId,
echo: JsonElement = EmptyJsonString
): String {
kotlin.runCatching {
var uid: String? = null
var groupUin: String? = null
var uid: String? = null
var groupUin: String? = null
var i = -1
val desc = MutableList(messages.size) { "" }
var i = -1
val desc = MutableList(messages.size) { "" }
val msgs = messages.map { msg ->
val msgs = messages.map { msg ->
kotlin.runCatching {
val data = msg.asJsonObject["data"].asJsonObject
if (data.containsKey("id")) {
val record = MsgSvc.getMsg(data["id"].asInt).getOrElse {
LogCenter.log("合并转发消息节点消息(id = ${data["id"].asInt})获取失败:$it", Level.WARN)
return@map null
error("合并转发消息节点消息(id = ${data["id"].asInt})获取失败:$it")
}
if (record.chatType == MsgConstant.KCHATTYPEGROUP) groupUin = record.peerUin.toString()
if (record.chatType == MsgConstant.KCHATTYPEC2C) uid = record.peerUid
@ -147,21 +146,21 @@ internal object SendForwardMessage : IActionHandler() {
).also {
desc[++i] = record.sendMemberName.ifEmpty { record.sendNickName } + ": "
}.map {
when (it.type) {
"text" -> desc[i] += it.data["text"] as String
"at" -> desc[i] += "@${it.data["name"] as String? ?: it.data["qq"] as String}"
"face" -> desc[i] += "[表情]"
"voice" -> desc[i] += "[语音]"
"node" -> desc[i] += "[合并转发消息]"
desc[++i] += when (it.type) {
"text" -> it.data["text"] as String
"at" -> "@${it.data["name"] as String? ?: it.data["qq"] as String}"
"face" -> "[表情]"
"voice" -> "[语音]"
"node" -> "[合并转发消息]"
"markdown" -> "[Markdown消息]"
"button" -> "[Button类型]"
else -> "[未知消息类型]"
}
it.toJson()
}.json
).also {
if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。")
if (it.second.isEmpty() && !it.first)
error("消息合成失败,请查看日志或者检查输入。")
}.second
)
)
@ -198,84 +197,91 @@ internal object SendForwardMessage : IActionHandler() {
body = MsgBody(
richText = RichText(
elements = MessageHelper.messageArrayToMessageElements(
1,
Random.nextLong(),
data["uin"]?.asString ?: TicketSvc.getUin(),
when (data["content"]) {
chatType = MsgConstant.KCHATTYPEGROUP,
msgId = Random.nextLong(),
peerId = data["uin"]?.asString ?: TicketSvc.getUin(),
messageList = when (data["content"]) {
is JsonObject -> listOf(data["content"] as JsonObject).json
is JsonArray -> data["content"] as JsonArray
else -> MessageHelper.decodeCQCode(data["content"].asString)
}.also {
desc[++i] = "${
data["name"].asStringOrNull ?: data["uin"].asStringOrNull
?: TicketSvc.getNickname()
}: "
desc[++i] =
(data["name"].asStringOrNull ?: data["uin"].asStringOrNull
?: TicketSvc.getNickname() )+ ": "
}.onEach {
val type = it.asJsonObject["type"].asString
val itData = it.asJsonObject["data"].asJsonObject
when (type) {
"text" -> desc[i] += itData["text"].asString
"at" -> desc[i] += "@${itData["name"].asStringOrNull ?: itData["qq"].asString}"
"face" -> desc[i] += "[表情]"
"image" -> desc[i] += "[图片]"
"voice" -> desc[i] += "[语音]"
"node" -> desc[i] += "[合并转发消息]"
desc[i] += when (type) {
"text" -> itData["text"].asString
"at" -> "@${itData["name"].asStringOrNull ?: itData["qq"].asString}"
"face" -> "[表情]"
"image" -> "[图片]"
"voice" -> "[语音]"
"node" -> "[合并转发消息]"
"markdown" -> "[Markdown消息]"
"button" -> "[Button类型]"
else -> "[未知消息类型]"
}
}
).also {
if (it.second.isEmpty() && !it.first) error("消息合成失败,请查看日志或者检查输入。")
if (it.second.isEmpty() && !it.first)
error("消息合成失败,请查看日志或者检查输入。")
}.second
)
)
)
} else {
LogCenter.log("消息节点缺少id或content字段", Level.WARN)
null
error("消息节点缺少id或content字段")
}
}.filterNotNull().ifEmpty { return logic("消息节点为空", echo) }
}.getOrElse {
LogCenter.log("消息节点解析失败:$it", Level.WARN)
null
}
}.filterNotNull().ifEmpty { return logic("消息节点为空", echo) }
val resid = MsgSvc.sendMultiMsg(uid ?: TicketSvc.getUid(), groupUin, msgs)
kotlin.runCatching {
val resid = MsgSvc.uploadMultiMsg(uid ?: TicketSvc.getUid(), groupUin, msgs)
.getOrElse { return logic(it.message ?: "", echo) }
val uniseq = UUID.randomUUID().toString().uppercase()
val result = MsgSvc.sendToAio(
chatType, peerId,
listOf(
hashMapOf(
mapOf(
"type" to "json",
"data" to hashMapOf(
"data" to hashMapOf(
"data" to mapOf(
"data" to mapOf(
"app" to "com.tencent.multimsg",
"config" to hashMapOf(
"config" to mapOf(
"autosize" to 1,
"forward" to 1,
"round" to 1,
"type" to "normal",
"width" to 300
).json,
),
"desc" to "[聊天记录]",
"extra" to hashMapOf(
"extra" to mapOf(
"filename" to uniseq,
"tsum" to 2
).json.toString(),
"meta" to hashMapOf(
"detail" to hashMapOf(
"meta" to mapOf(
"detail" to mapOf(
"news" to desc.slice(0..if (i < 3) i else 3)
.map { hashMapOf("text" to it).json }.json,
.map { mapOf("text" to it) },
"resid" to resid,
"source" to "群聊的聊天记录",
"summary" to "查看${msgs.size}条转发消息",
"uniseq" to uniseq
).json
).json,
)
),
"prompt" to "[聊天记录]",
"ver" to "0.0.0.5",
"view" to "contact"
).json,
),
"resid" to resid
).json
).json
)
)
).json, fromId, 3
).getOrElse { return logic(it.message ?: "", echo) }
@ -286,7 +292,7 @@ internal object SendForwardMessage : IActionHandler() {
), echo = echo
)
}.onFailure {
return error("error: $it", echo)
return error("合并转发消息失败: $it", echo)
}
return logic("合并转发消息失败(unknown error)", echo)
}

View File

@ -15,13 +15,13 @@ internal object SendGroupMessage: IActionHandler() {
return if (session.isString("message")) {
val autoEscape = session.getBooleanOrDefault("auto_escape", false)
val message = session.getString("message")
SendMessage(MsgConstant.KCHATTYPEGROUP, groupId.toString(), message, autoEscape, echo = session.echo, retryCnt = retryCnt ?: 3, recallDuration = recallDuration)
SendMessage(MsgConstant.KCHATTYPEGROUP, groupId.toString(), message, autoEscape, echo = session.echo, retryCnt = retryCnt ?: 5, recallDuration = recallDuration)
} else if (session.isObject("message")) {
val message = session.getObject("message")
SendMessage(MsgConstant.KCHATTYPEGROUP, groupId.toString(), listOf( message ).jsonArray, session.echo, retryCnt = retryCnt ?: 3, recallDuration = recallDuration)
SendMessage(MsgConstant.KCHATTYPEGROUP, groupId.toString(), listOf( message ).jsonArray, session.echo, retryCnt = retryCnt ?: 5, recallDuration = recallDuration)
} else {
val message = session.getArray("message")
SendMessage(MsgConstant.KCHATTYPEGROUP, groupId.toString(), message, session.echo, retryCnt = retryCnt ?: 3, recallDuration = recallDuration)
SendMessage(MsgConstant.KCHATTYPEGROUP, groupId.toString(), message, session.echo, retryCnt = retryCnt ?: 5, recallDuration = recallDuration)
}
}

View File

@ -55,13 +55,13 @@ internal object SendMessage: IActionHandler() {
return if (session.isString("message")) {
val autoEscape = session.getBooleanOrDefault("auto_escape", false)
val message = session.getString("message")
invoke(chatType, peerId, message, autoEscape, echo = session.echo, fromId = fromId, retryCnt = retryCnt ?: 3, recallDuration = recallDuration)
invoke(chatType, peerId, message, autoEscape, echo = session.echo, fromId = fromId, retryCnt = retryCnt ?: 5, recallDuration = recallDuration)
} else if (session.isArray("message")) {
val message = session.getArray("message")
invoke(chatType, peerId, message, session.echo, fromId = fromId, retryCnt ?: 3, recallDuration = recallDuration)
invoke(chatType, peerId, message, session.echo, fromId = fromId, retryCnt ?: 5, recallDuration = recallDuration)
} else {
val message = session.getObject("message")
invoke(chatType, peerId, listOf( message ).jsonArray, session.echo, fromId = fromId, retryCnt ?: 3, recallDuration = recallDuration)
invoke(chatType, peerId, listOf( message ).jsonArray, session.echo, fromId = fromId, retryCnt ?: 5, recallDuration = recallDuration)
}
} catch (e: ParamsException) {
return noParam(e.message!!, session.echo)

View File

@ -1,10 +1,12 @@
package moe.fuqiuluo.shamrock.remote.action.handlers
import kotlinx.atomicfu.atomic
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.qqinterface.servlet.BaseSvc
import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.symbols.OneBotHandler
import protobuf.auto.toByteArray
import protobuf.message.*
@ -19,13 +21,19 @@ internal object SendMsgByResid : IActionHandler() {
private val msgSeq = atomic(1000)
override suspend fun internalHandle(session: ActionSession): String {
val resid = session.getString("resid")
val peerId = session.getString("peer")
val resId = session.getString("res_id")
val peerId = session.getString("peer_id")
val messageType = session.getString("message_type")
invoke(resId, peerId, messageType)
return ok("ok", session.echo)
}
suspend operator fun invoke(peerId: String, resId: String, messageType: String, echo: JsonElement = EmptyJsonString): String {
val req = PbSendMsgReq(
routingHead = when (session.getStringOrNull("message_type")) {
"group" ->RoutingHead(grp = Grp(peerId.toUInt()))
"private" ->RoutingHead( c2c = C2C(peerId.toUInt()))
else ->RoutingHead( grp = Grp(peerId.toUInt()))
routingHead = when (messageType) {
"group" -> RoutingHead(grp = Grp(peerId.toUInt()))
"private" -> RoutingHead(c2c = C2C(peerId.toUInt()))
else -> RoutingHead(grp = Grp(peerId.toUInt()))
},
contentHead = ContentHead(1, 0, 0, 0),
msgBody = MsgBody(
@ -34,7 +42,7 @@ internal object SendMsgByResid : IActionHandler() {
Elem(
generalFlags = GeneralFlags(
longTextFlag = 1u,
longTextResid = resid.toByteArray()
longTextResid = resId
)
)
)
@ -45,6 +53,6 @@ internal object SendMsgByResid : IActionHandler() {
msgVia = 0u
)
BaseSvc.sendBufferAW("MessageSvc.PbSendMsg", true, req.toByteArray())
return ok("ok", session.echo)
return ok("ok", echo)
}
}

View File

@ -24,7 +24,7 @@ internal object SendPrivateMessage : IActionHandler() {
autoEscape = autoEscape,
echo = session.echo,
fromId = groupId?.toString() ?: userId.toString(),
retryCnt = retryCnt ?: 3,
retryCnt = retryCnt ?: 5,
recallDuration = recallDuration
)
} else {
@ -34,7 +34,7 @@ internal object SendPrivateMessage : IActionHandler() {
message = if (session.isArray("message")) session.getArray("message") else listOf(session.getObject("message")).jsonArray,
echo = session.echo,
fromId = groupId?.toString() ?: userId.toString(),
retryCnt = retryCnt ?: 3,
retryCnt = retryCnt ?: 5,
recallDuration = recallDuration
)
}

View File

@ -1,59 +1,55 @@
package moe.fuqiuluo.shamrock.remote.api
import io.ktor.server.routing.Routing
import com.tencent.qqnt.kernel.nativeinterface.MsgConstant
import com.tencent.qqnt.kernel.nativeinterface.MsgElement
import com.tencent.qqnt.kernel.nativeinterface.TextElement
import io.ktor.server.application.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import moe.fuqiuluo.qqinterface.servlet.TicketSvc
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.tools.ShamrockVersion
import moe.fuqiuluo.shamrock.helper.MessageHelper
import moe.fuqiuluo.shamrock.remote.action.handlers.SendMsgByResid
import moe.fuqiuluo.shamrock.remote.service.config.ShamrockConfig
import moe.fuqiuluo.shamrock.tools.fetchOrThrow
import moe.fuqiuluo.shamrock.tools.getOrPost
import moe.fuqiuluo.shamrock.xposed.helper.NTServiceFetcher
fun Routing.testAction() {
if(ShamrockVersion.contains("dev")) {
if (ShamrockConfig.isDev()) {
LogCenter.log("testAction is enabled.", Level.WARN)
} else {
return
}
/*
get("/test/createUidFromTinyId") {
val selfId = fetchOrThrow("selfId").toLong()
val peerId = fetchOrThrow("peerId").toLong()
call.respondText(NTServiceFetcher.kernelService.wrapperSession.msgService.createUidFromTinyId(selfId, peerId))
getOrPost("/send_msg_by_resid") {
val resId = fetchOrThrow("res_id")
val peerId = fetchOrThrow("peer_Id")
val messageType = fetchOrThrow("message_type")
call.respondText(SendMsgByResid(resId, peerId, messageType))
}
get("/test/addSendMsg") {
getOrPost("/createUidFromTinyId") {
val selfId = fetchOrThrow("selfId").toLong()
val peerId = fetchOrThrow("peerId")
call.respondText(NTServiceFetcher.kernelService.wrapperSession.msgService.createUidFromTinyId(selfId, peerId.toLong()))
}
getOrPost("/addSendMsg") {
val msgService = NTServiceFetcher.kernelService.wrapperSession.msgService
val msgId = msgService.getMsgUniqueId(System.currentTimeMillis())
msgService.addSendMsg(msgId, MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, TicketSvc.getUin()), arrayListOf(
MsgElement().apply {
elementType = MsgConstant.KELEMTYPETEXT
textElement = TextElement().apply {
content = "测试消息"
}
}
), hashMapOf())
call.respondText("ok")
}*/
/*
get("/test/getMsgs") {
kotlin.runCatching {
val msgService = NTServiceFetcher.kernelService.wrapperSession.msgService
val msgs = suspendCoroutine {
msgService.getMsgs(Contact(MsgConstant.KCHATTYPEGROUP, "884587317", ""), 0L, 20, true, object: IMsgOperateCallback{
override fun onResult(code: Int, why: String?, msgs: ArrayList<MsgRecord>?) {
it.resume(msgs)
msgService.addSendMsg(msgId,
MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, TicketSvc.getUin()),
arrayListOf(
MsgElement().apply {
elementType = MsgConstant.KELEMTYPETEXT
textElement = TextElement().apply {
content = "测试消息"
}
})
}
if (msgs == null) {
call.respondText("failed")
return@get
}
call.respondText("msg -> " + msgs.map { it.toCQCode() }.joinToString("\n"))
}.onFailure {
call.respondText("failed: ${it.stackTraceToString()}")
return@get
}
}*/
}
),
hashMapOf())
call.respondText("ok")
}
}

View File

@ -12,7 +12,6 @@ import moe.fuqiuluo.qqinterface.servlet.FriendSvc.requestFriendSystemMsgNew
import moe.fuqiuluo.qqinterface.servlet.GroupSvc
import moe.fuqiuluo.qqinterface.servlet.GroupSvc.requestGroupSystemMsgNew
import moe.fuqiuluo.qqinterface.servlet.TicketSvc.getLongUin
import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
import moe.fuqiuluo.shamrock.helper.ContactHelper
import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter
@ -30,11 +29,7 @@ import moe.fuqiuluo.symbols.decodeProtobuf
import protobuf.message.ContentHead
import protobuf.message.MsgBody
import protobuf.message.ResponseHead
import protobuf.message.multimedia.RichMediaForPicData
import protobuf.push.*
import java.util.regex.Pattern
private val RKEY_PATTERN = Pattern.compile("rkey=([A-Za-z0-9_-]+)")
internal object PrimitiveListener {
fun registerListener() {
@ -94,22 +89,9 @@ internal object PrimitiveListener {
}
private fun onGroupMessage(msgTime: Long, body: MsgBody) {
/*runCatching {
body.richText?.elements?.filter {
it.commonElem != null && it.commonElem!!.serviceType == 48
}?.map {
it.commonElem!!.elem!!.decodeProtobuf<RichMediaForPicData>()
}?.forEach {
it.display?.show?.download?.url?.let {
RKEY_PATTERN.matcher(it).takeIf {
it.find()
}?.group(1)?.let { rkey ->
LogCenter.log("更新NT RKEY成功$rkey")
RichProtoSvc.multiMediaRKey = rkey
}
}
}
}*/
runCatching {
}
}
private suspend fun onC2CPoke(msgTime: Long, body: MsgBody) {

View File

@ -42,35 +42,39 @@ val String.asJson: JsonElement
val String.asJsonObject: JsonObject
get() = Json.parseToJsonElement(this).asJsonObject
val Collection<Any>.json: JsonArray
val Collection<Any?>.json: JsonArray
get() {
val arrayList = arrayListOf<JsonElement>()
forEach {
when (it) {
is JsonElement -> arrayList.add(it)
is Number -> arrayList.add(it.json)
is String -> arrayList.add(it.json)
is Boolean -> arrayList.add(it.json)
is Map<*, *> -> arrayList.add((it as Map<String, Any>).json)
is Collection<*> -> arrayList.add((it as Collection<Any>).json)
else -> error("unknown array type: ${it::class.java}")
if (it != null) {
when (it) {
is JsonElement -> arrayList.add(it)
is Number -> arrayList.add(it.json)
is String -> arrayList.add(it.json)
is Boolean -> arrayList.add(it.json)
is Map<*, *> -> arrayList.add((it as Map<String, Any?>).json)
is Collection<*> -> arrayList.add((it as Collection<Any?>).json)
else -> error("unknown array type: ${it::class.java}")
}
}
}
return arrayList.jsonArray
}
val Map<String, Any>.json: JsonObject
val Map<String, Any?>.json: JsonObject
get() {
val map = hashMapOf<String, JsonElement>()
forEach { (key, any) ->
when (any) {
is JsonElement -> map[key] = any
is Number -> map[key] = any.json
is String -> map[key] = any.json
is Boolean -> map[key] = any.json
is Map<*, *> -> map[key] = (any as Map<String, Any>).json
is Collection<*> -> map[key] = (any as Collection<Any>).json
else -> error("unknown object type: ${any::class.java}")
if (any != null) {
when (any) {
is JsonElement -> map[key] = any
is Number -> map[key] = any.json
is String -> map[key] = any.json
is Boolean -> map[key] = any.json
is Map<*, *> -> map[key] = (any as Map<String, Any?>).json
is Collection<*> -> map[key] = (any as Collection<Any?>).json
else -> error("unknown object type: ${any::class.java}")
}
}
}
return map.jsonObject

View File

@ -117,6 +117,8 @@ internal class XposedEntry: IXposedHookLoadPackage {
}
private fun execStartupInit(ctx: Context) {
log("Shamrock: Executing startup init: $ctx")
if (sec_static_stage_inited) return
val classLoader = ctx.classLoader.also { requireNotNull(it) }