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 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.tencent.** { *; }
-keep class com.qq.** { *; } -keep class com.qq.** { *; }
-keep class com.google.gson.** { *; } -keep class com.google.gson.** { *; }
-keep class de.** { *; } -keep class de.** { *; }
-keep class epic.** { *; }
-keep class friendlist.** { *; }
-keep class KQQ.** { *; }
-keep class mqq.** { *; } -keep class mqq.** { *; }
-keep class msf.** { *; }
-keep class oicq.** { *; }
-keep class QQService.** { *; } -keep class QQService.** { *; }
-keep class SummaryCard.** { *; } -keep class SummaryCard.** { *; }
-keep class tencent.** { *; } -keep class tencent.** { *; }
-keep class VIP.** { *; }
-keepclassmembers class * { -keepclassmembers class * {
native <methods>; native <methods>;
} }
-keep class io.netty.** { *; } -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.Test
import org.junit.Assert.*
/** /**
* Example local unit test, which will execute on the development machine (host). * Example local unit test, which will execute on the development machine (host).
* *
* See [testing documentation](http://d.android.com/tools/testing). * See [testing documentation](http://d.android.com/tools/testing).
*/ */
class ExampleUnitTest { class ExampleUnitTest {
@Test @Test
fun addition_isCorrect() { fun test() {
assertEquals(4, 2 + 2)
} }
} }

View File

@ -15,7 +15,7 @@ data class ContentHead(
@ProtoNumber(8) val u6: Int? = null, @ProtoNumber(8) val u6: Int? = null,
@ProtoNumber(9) val u7: Int? = null, @ProtoNumber(9) val u7: Int? = null,
@ProtoNumber(11) val msgSeq: Long? = 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(14) val u4: Long? = null,
@ProtoNumber(15) val forwardHead: ForwardHead? = null, @ProtoNumber(15) val forwardHead: ForwardHead? = null,
@ProtoNumber(28) val u5: Long? = null @ProtoNumber(28) val u5: Long? = null

View File

@ -30,7 +30,7 @@ data class CustomFace(
@ProtoNumber(23) var height: UInt? = null, @ProtoNumber(23) var height: UInt? = null,
@ProtoNumber(24) var source: UInt? = null, @ProtoNumber(24) var source: UInt? = null,
@ProtoNumber(25) var size: 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(27) var thumbWidth: UInt? = null,
@ProtoNumber(28) var thumbHeight: UInt? = null, @ProtoNumber(28) var thumbHeight: UInt? = null,
@ProtoNumber(29) var showLen: UInt? = null, @ProtoNumber(29) var showLen: UInt? = null,

View File

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

View File

@ -12,7 +12,7 @@ data class GeneralFlags(
@ProtoNumber(4) val rpId: ByteArray? = null, @ProtoNumber(4) val rpId: ByteArray? = null,
@ProtoNumber(5) val prpFold: UInt? = null, @ProtoNumber(5) val prpFold: UInt? = null,
@ProtoNumber(6) val longTextFlag: 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(8) val groupType: UInt? = null,
@ProtoNumber(9) val toUinFlag: UInt? = null, @ProtoNumber(9) val toUinFlag: UInt? = null,
@ProtoNumber(10) val glamourLevel: UInt? = null, @ProtoNumber(10) val glamourLevel: UInt? = null,

View File

@ -14,10 +14,10 @@ data class NotOnlineImage(
@ProtoNumber(7) val picMd5: ByteArray? = null, @ProtoNumber(7) val picMd5: ByteArray? = null,
@ProtoNumber(8) val picHeight: UInt? = null, @ProtoNumber(8) val picHeight: UInt? = null,
@ProtoNumber(9) val picWidth: 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(11) val flag: ByteArray? = null,
@ProtoNumber(12) val thumbUrl: String? = 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(14) val bigUrl: String? = null,
@ProtoNumber(15) val origUrl: String? = null, @ProtoNumber(15) val origUrl: String? = null,
@ProtoNumber(16) val bizType: UInt? = null, @ProtoNumber(16) val bizType: UInt? = null,

View File

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

View File

@ -10,5 +10,12 @@ data class TextMsg(
@ProtoNumber(3) val attr6Buf: ByteArray? = null, @ProtoNumber(3) val attr6Buf: ByteArray? = null,
@ProtoNumber(4) val attr7Buf: ByteArray? = null, @ProtoNumber(4) val attr7Buf: ByteArray? = null,
@ProtoNumber(11) val buf: 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 @Serializable
data class Button( data class Button(
@ProtoNumber(1) val id: Int? = null, @ProtoNumber(1) val id: String? = null,
@ProtoNumber(2) val renderData: RenderData? = null, @ProtoNumber(2) val renderData: RenderData? = null,
@ProtoNumber(3) val action: Action? = null, @ProtoNumber(3) val action: Action? = null,
) )
@ -41,8 +41,8 @@ data class Action(
@ProtoNumber(2) val permission: Permission? = null, @ProtoNumber(2) val permission: Permission? = null,
@ProtoNumber(4) val unsupportTips: String? = null, @ProtoNumber(4) val unsupportTips: String? = null,
@ProtoNumber(5) val data: String? = null, @ProtoNumber(5) val data: String? = null,
@ProtoNumber(6) val reply: Boolean? = null, @ProtoNumber(7) val reply: Boolean? = null,
@ProtoNumber(7) val enter: Boolean? = null, @ProtoNumber(8) val enter: Boolean? = null,
) )
@Serializable @Serializable

View File

@ -11,7 +11,7 @@ data class QFaceExtra(
@ProtoNumber(3) val faceId: Int? = null, @ProtoNumber(3) val faceId: Int? = null,
@ProtoNumber(4) val field4: Int? = null, @ProtoNumber(4) val field4: Int? = null,
@ProtoNumber(5) val field5: 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(7) val faceText: String? = null,
@ProtoNumber(9) val field9: Int? = null @ProtoNumber(9) val field9: Int? = null
) : Protobuf<QFaceExtra> ) : 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; package com.tencent.mobileqq.transfile;
public class BaseTransProcessor { import com.tencent.mobileqq.utils.httputils.IHttpCommunicatorListener;
public class BaseTransProcessor implements IHttpCommunicatorListener {
public FileMsg file; public FileMsg file;
public long getFileStatus() { 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; package com.tencent.mobileqq.transfile;
import java.io.File;
import java.io.InputStream;
import java.io.OutputStream;
public class FileMsg { public class FileMsg {
public static final int STATUS_FILE_EXPIRED = 5002; public static final int STATUS_FILE_EXPIRED = 5002;
public static final int STATUS_FILE_TRANSFERING = 5000; 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_BUDDY = 0;
public static final int UIN_DISCUSS = 2; public static final int UIN_DISCUSS = 2;
public static final int UIN_TROOP = 1; 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; 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; this.isFlashPic = bool;
} }
public void setStoreID(int i2) {
}
public void setMd5HexStr(String str) { public void setMd5HexStr(String str) {
this.md5HexStr = str; this.md5HexStr = str;
} }

View File

@ -18,182 +18,4 @@
# If you keep the line number information, uncomment this to # If you keep the line number information, uncomment this to
# hide the original source file name. # hide the original source file name.
#-renamesourcefileattribute SourceFile #-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.** {
*;
}

View File

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

View File

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

View File

@ -8,10 +8,10 @@ import moe.fuqiuluo.shamrock.tools.json
internal data class MessageSegment( internal data class MessageSegment(
val type: String, val type: String,
val data: Map<String, Any> = emptyMap() val data: Map<String, Any?> = emptyMap()
) { ) {
fun toJson(): JsonObject { fun toJson(): JsonObject {
return hashMapOf( return mapOf(
"type" to type.json, "type" to type.json,
"data" to data.json "data" to data.json
).json ).json
@ -26,7 +26,7 @@ internal fun List<MessageSegment>.toJson(): JsonArray {
internal fun List<MessageSegment>.toListMap(): List<Map<String, JsonElement>> { internal fun List<MessageSegment>.toListMap(): List<Map<String, JsonElement>> {
return this.map { return this.map {
hashMapOf( mapOf(
"type" to it.type.json, "type" to it.type.json,
"data" to it.data.json "data" to it.data.json
).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.GroupSvc
import moe.fuqiuluo.qqinterface.servlet.MsgSvc import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.qqinterface.servlet.TicketSvc 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.*
import moe.fuqiuluo.qqinterface.servlet.transfile.PictureResource import moe.fuqiuluo.qqinterface.servlet.transfile.PictureResource
import moe.fuqiuluo.qqinterface.servlet.transfile.Private 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.Level
import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.helper.LogicException import moe.fuqiuluo.shamrock.helper.LogicException
import moe.fuqiuluo.shamrock.helper.MessageHelper.messageArrayToMessageElements
import moe.fuqiuluo.shamrock.helper.ParamsException import moe.fuqiuluo.shamrock.helper.ParamsException
import moe.fuqiuluo.shamrock.tools.* import moe.fuqiuluo.shamrock.tools.*
import moe.fuqiuluo.shamrock.utils.DeflateTools 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 typealias IMessageElementMaker = suspend (Int, Long, String, JsonObject) -> Result<Elem>
internal object MessageElementMaker { internal object ElemMaker {
private val makerArray = hashMapOf( private val makerArray = hashMapOf(
"text" to MessageElementMaker::createTextElem, "text" to ElemMaker::createTextElem,
"at" to MessageElementMaker::createAtElem, "at" to ElemMaker::createAtElem,
"face" to MessageElementMaker::createFaceElem, "face" to ElemMaker::createFaceElem,
"pic" to MessageElementMaker::createImageElem, "pic" to ElemMaker::createImageElem,
"image" to MessageElementMaker::createImageElem, "image" to ElemMaker::createImageElem,
// "voice" to MessageElementMaker::createRecordElem, // "voice" to MessageElementMaker::createRecordElem,
// "record" to MessageElementMaker::createRecordElem, // "record" to MessageElementMaker::createRecordElem,
// "video" to MessageElementMaker::createVideoElem, // "video" to MessageElementMaker::createVideoElem,
"markdown" to MessageElementMaker::createMarkdownElem, "markdown" to ElemMaker::createMarkdownElem,
"button" to MessageElementMaker::createButtonElem, "button" to ElemMaker::createButtonElem,
"inline_keyboard" to MessageElementMaker::createButtonElem, "inline_keyboard" to ElemMaker::createButtonElem,
// "dice" to MessageElementMaker::createDiceElem, "dice" to ElemMaker::createNewDiceElem,
// "rps" to MessageElementMaker::createRpsElem, "rps" to ElemMaker::createNewRpsElem,
"basketball" to MessageElementMaker::createBasketballElem, "poke" to ElemMaker::createPokeElem,
"new_dice" to MessageElementMaker::createNewDiceElem,
"new_rps" to MessageElementMaker::createNewRpsElem,
"poke" to MessageElementMaker::createPokeElem,
// "anonymous" to MessageElementMaker::createAnonymousElem, // "anonymous" to MessageElementMaker::createAnonymousElem,
// "share" to MessageElementMaker::createShareElem, // "share" to MessageElementMaker::createShareElem,
// "contact" to MessageElementMaker::createContactElem, // "contact" to MessageElementMaker::createContactElem,
// "location" to MessageElementMaker::createLocationElem, // "location" to MessageElementMaker::createLocationElem,
// "music" to MessageElementMaker::createMusicElem, // "music" to MessageElementMaker::createMusicElem,
"reply" to MessageElementMaker::createReplyElem, "reply" to ElemMaker::createReplyElem,
// "touch" to MessageElementMaker::createTouchElem, // "touch" to MessageElementMaker::createTouchElem,
// "weather" to MessageElementMaker::createWeatherElem, "weather" to ElemMaker::createWeatherElem,
"json" to MessageElementMaker::createJsonElem, "json" to ElemMaker::createJsonElem,
//"node" to MessageMaker::createNodeElem, // "node" to MessageMaker::createNodeElem,
//"multi_msg" to MessageMaker::createLongMsgStruct, //"multi_msg" to MessageMaker::createLongMsgStruct,
//"bubble_face" to MessageElementMaker::createBubbleFaceElem, //"bubble_face" to MessageElementMaker::createBubbleFaceElem,
) )
@ -111,7 +112,11 @@ internal object MessageElementMaker {
else -> { else -> {
qq = qqStr.toLong() qq = qqStr.toLong()
type = 0 type = 0
"@" + (data["name"].asStringOrNull ?: GroupSvc.getTroopMemberInfoByUinV2(peerId.toLong(), qq, true) "@" + (data["name"].asStringOrNull ?: GroupSvc.getTroopMemberInfoByUinV2(
peerId.toLong(),
qq,
true
)
.let { .let {
val info = it.getOrNull() val info = it.getOrNull()
if (info == null) if (info == null)
@ -164,9 +169,31 @@ internal object MessageElementMaker {
data: JsonObject data: JsonObject
): Result<Elem> { ): Result<Elem> {
data.checkAndThrow("id") data.checkAndThrow("id")
val elem = Elem( val faceId = data["id"].asInt
face = FaceMsg(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) return Result.success(elem)
} }
@ -259,7 +286,7 @@ internal object MessageElementMaker {
width = picWidth.toUInt(), width = picWidth.toUInt(),
height = picHeight.toUInt(), height = picHeight.toUInt(),
size = QQNTWrapperUtil.CppProxy.getFileSize(file.absolutePath).toUInt(), size = QQNTWrapperUtil.CppProxy.getFileSize(file.absolutePath).toUInt(),
origin = if (isOriginal) 1u else 0u, origin = isOriginal,
thumbWidth = 0u, thumbWidth = 0u,
thumbHeight = 0u, thumbHeight = 0u,
pbReserve = CustomFace.Companion.PbReserve(field1 = 0) pbReserve = CustomFace.Companion.PbReserve(field1 = 0)
@ -279,7 +306,7 @@ internal object MessageElementMaker {
picHeight = picWidth.toUInt(), picHeight = picWidth.toUInt(),
picWidth = picHeight.toUInt(), picWidth = picHeight.toUInt(),
resId = "".toByteArray(), resId = "".toByteArray(),
original = if (isOriginal) 1u else 0u, // true original = isOriginal, // true
pbReserve = NotOnlineImage.Companion.PbReserve(field1 = 0) pbReserve = NotOnlineImage.Companion.PbReserve(field1 = 0)
) )
) )
@ -324,7 +351,7 @@ internal object MessageElementMaker {
), ),
type = 0u, type = 0u,
pbReserve = SourceMsg.Companion.PbReserve( pbReserve = SourceMsg.Companion.PbReserve(
field3 = Random.nextULong(), msgRand = Random.nextInt().toULong(),
field8 = Random.nextInt(0, 10000) field8 = Random.nextInt(0, 10000)
), ),
) )
@ -340,10 +367,19 @@ internal object MessageElementMaker {
senderUin = msg.senderUin.toULong(), senderUin = msg.senderUin.toULong(),
time = msg.msgTime.toULong(), time = msg.msgTime.toULong(),
flag = 1u, 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, type = 0u,
pbReserve = SourceMsg.Companion.PbReserve( pbReserve = SourceMsg.Companion.PbReserve(
field3 = Random.nextULong(), msgRand = Random.nextULong(),
senderUid = msg.senderUid, senderUid = msg.senderUid,
receiverUid = TicketSvc.getUid(), receiverUid = TicketSvc.getUid(),
field8 = Random.nextInt(0, 10000) field8 = Random.nextInt(0, 10000)
@ -370,6 +406,38 @@ internal object MessageElementMaker {
return Result.success(elem) 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( private suspend fun createPokeElem(
chatType: Int, chatType: Int,
msgId: Long, msgId: Long,
@ -391,31 +459,6 @@ internal object MessageElementMaker {
return Result.success(elem) 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( private suspend fun createNewDiceElem(
chatType: Int, chatType: Int,
msgId: Long, msgId: Long,
@ -431,7 +474,7 @@ internal object MessageElementMaker {
faceId = 358, faceId = 358,
field4 = 1, field4 = 1,
field5 = 2, field5 = 2,
field6 = "", result = "",
faceText = "/骰子", faceText = "/骰子",
field9 = 1 field9 = 1
).toByteArray(), ).toByteArray(),
@ -456,7 +499,7 @@ internal object MessageElementMaker {
faceId = 359, faceId = 359,
field4 = 1, field4 = 1,
field5 = 2, field5 = 2,
field6 = "", result = "",
faceText = "/包剪锤", faceText = "/包剪锤",
field9 = 1 field9 = 1
).toByteArray(), ).toByteArray(),
@ -489,19 +532,20 @@ internal object MessageElementMaker {
peerId: String, peerId: String,
data: JsonObject data: JsonObject
): Result<Elem> { ): Result<Elem> {
data.checkAndThrow("rows") data.checkAndThrow("buttons")
val elem = Elem( val elem = Elem(
commonElem = CommonElem( commonElem = CommonElem(
serviceType = 46, serviceType = 46,
elem = ButtonExtra( elem = ButtonExtra(
field1 = Object1( field1 = Object1(
rows = data["rows"].asJsonArray.map { row -> rows = data["buttons"].asJsonArray.map { row ->
Row(buttons = row.asJsonArray.map { Row(buttons = row.asJsonArray.map {
val button = it.asJsonObject val button = it.asJsonObject
val renderData = button["render_data"].asJsonObject val renderData = button["render_data"].asJsonObject
val action = button["action"].asJsonObject val action = button["action"].asJsonObject
val permission = action["permission"].asJsonObject
Button( Button(
id = button["id"].asIntOrNull, id = button["id"].asStringOrNull,
renderData = RenderData( renderData = RenderData(
label = renderData["label"].asString, label = renderData["label"].asString,
visitedLabel = renderData["visited_label"].asString, visitedLabel = renderData["visited_label"].asString,
@ -510,9 +554,9 @@ internal object MessageElementMaker {
action = Action( action = Action(
type = action["type"].asInt, type = action["type"].asInt,
permission = Permission( permission = Permission(
type = action["permission"].asJsonObject["type"].asInt, type = permission["type"].asInt,
specifyRoleIds = action["permission"].asJsonObject["specify_role_ids"].asJsonArrayOrNull?.map { id -> id.asString }, specifyRoleIds = permission["specify_role_ids"].asJsonArrayOrNull?.map { id -> id.asString },
specifyUserIds = action["permission"].asJsonObject["specify_user_ids"].asJsonArrayOrNull?.map { id -> id.asString } specifyUserIds = permission["specify_user_ids"].asJsonArrayOrNull?.map { id -> id.asString }
), ),
unsupportTips = action["unsupport_tips"].asString, unsupportTips = action["unsupport_tips"].asString,
data = action["data"].asString, data = action["data"].asString,

View File

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

View File

@ -1,5 +1,7 @@
package moe.fuqiuluo.qqinterface.servlet.transfile package moe.fuqiuluo.qqinterface.servlet.transfile
import com.tencent.mobileqq.data.MessageRecord
internal enum class ContactType { internal enum class ContactType {
TROOP, TROOP,
PRIVATE, PRIVATE,
@ -8,12 +10,20 @@ internal enum class ContactType {
internal interface TransTarget { internal interface TransTarget {
val id: String val id: String
val type: ContactType 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 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 override val type: ContactType = ContactType.PRIVATE
} }

View File

@ -2,15 +2,18 @@
package moe.fuqiuluo.qqinterface.servlet.transfile package moe.fuqiuluo.qqinterface.servlet.transfile
import com.tencent.mobileqq.transfile.BaseTransProcessor
import com.tencent.mobileqq.transfile.FileMsg import com.tencent.mobileqq.transfile.FileMsg
import com.tencent.mobileqq.transfile.TransferRequest import com.tencent.mobileqq.transfile.TransferRequest
import com.tencent.mobileqq.transfile.api.ITransFileController import com.tencent.mobileqq.transfile.api.ITransFileController
import com.tencent.mobileqq.utils.httputils.IHttpCommunicatorListener
import kotlinx.coroutines.DelicateCoroutinesApi import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.suspendCancellableCoroutine import kotlinx.coroutines.suspendCancellableCoroutine
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import moe.fuqiuluo.shamrock.helper.LogCenter
import moe.fuqiuluo.shamrock.utils.MD5 import moe.fuqiuluo.shamrock.utils.MD5
import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher import moe.fuqiuluo.shamrock.xposed.helper.AppRuntimeFetcher
import mqq.app.AppRuntime import mqq.app.AppRuntime
@ -81,6 +84,7 @@ internal abstract class FileTransfer {
} }
suspendCancellableCoroutine { continuation -> suspendCancellableCoroutine { continuation ->
GlobalScope.launch { GlobalScope.launch {
lateinit var processor: IHttpCommunicatorListener
while ( while (
//service.findProcessor( //service.findProcessor(
// transferRequest.keyForTransfer // uin + uniseq // transferRequest.keyForTransfer // uin + uniseq
@ -89,8 +93,13 @@ internal abstract class FileTransfer {
// 如果上传处理器依旧存在,说明没有上传成功 // 如果上传处理器依旧存在,说明没有上传成功
&& service.isWorking.get() && service.isWorking.get()
) { ) {
processor = service.findProcessor(runtime.currentAccountUin, transferRequest.mUniseq)
delay(100) 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) 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.coroutines.suspendCancellableCoroutine
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import moe.fuqiuluo.qqinterface.servlet.BaseSvc 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.ContactHelper
import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.helper.LogCenter
@ -53,8 +54,6 @@ private const val MULTIMEDIA_DOMAIN = "multimedia.nt.qq.com.cn"
private const val C2C_PIC = "c2cpicdw.qpic.cn" private const val C2C_PIC = "c2cpicdw.qpic.cn"
internal object RichProtoSvc: BaseSvc() { internal object RichProtoSvc: BaseSvc() {
private val requestId = atomic(2L)
suspend fun getGuildFileDownUrl(peerId: String, channelId: String, fileId: String, bizId: Int): String { suspend fun getGuildFileDownUrl(peerId: String, channelId: String, fileId: String, bizId: Int): String {
val buffer = sendOidbAW("OidbSvcTrpcTcp.0xfc2_0", 4034, 0, Oidb0xfc2ReqBody( val buffer = sendOidbAW("OidbSvcTrpcTcp.0xfc2_0", 4034, 0, Oidb0xfc2ReqBody(
msgCmd = 1200, msgCmd = 1200,
@ -105,7 +104,7 @@ internal object RichProtoSvc: BaseSvc() {
val domain = if (!result.download_file_rsp.str_download_dns.has()) val domain = if (!result.download_file_rsp.str_download_dns.has())
("https://" + result.download_file_rsp.str_download_ip.get()) ("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 downloadUrl = result.download_file_rsp.bytes_download_url.get().toByteArray().toHexString()
val appId = MobileQQ.getMobileQQ().appId val appId = MobileQQ.getMobileQQ().appId
val version = PlatformUtils.getQQVersion(MobileQQ.getContext()) 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" 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( suspend fun getC2CVideoDownUrl(
peerId: String, peerId: String,
md5: ByteArray, md5: ByteArray,

View File

@ -1,6 +1,8 @@
package moe.fuqiuluo.qqinterface.servlet.transfile package moe.fuqiuluo.qqinterface.servlet.transfile
import com.tencent.mobileqq.data.MessageForPic
import com.tencent.mobileqq.data.MessageForShortVideo import com.tencent.mobileqq.data.MessageForShortVideo
import com.tencent.mobileqq.data.MessageRecord
import com.tencent.mobileqq.transfile.FileMsg import com.tencent.mobileqq.transfile.FileMsg
import com.tencent.mobileqq.transfile.TransferRequest import com.tencent.mobileqq.transfile.TransferRequest
import moe.fuqiuluo.shamrock.utils.MD5 import moe.fuqiuluo.shamrock.utils.MD5
@ -11,16 +13,15 @@ import moe.fuqiuluo.shamrock.helper.TransfileHelper
internal object Transfer: FileTransfer() { internal object Transfer: FileTransfer() {
private val ROUTE = mapOf<ContactType, Map<ResourceType, suspend TransTarget.(Resource) -> Boolean>>( private val ROUTE = mapOf<ContactType, Map<ResourceType, suspend TransTarget.(Resource) -> Boolean>>(
ContactType.TROOP to mapOf( 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) }, Voice to { uploadGroupVoice(id, (it as VoiceResource).src) },
Video to { uploadGroupVideo(id, (it as VideoResource).src, it.thumb) }, Video to { uploadGroupVideo(id, (it as VideoResource).src, it.thumb) },
), ),
ContactType.PRIVATE to mapOf( 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) }, Voice to { uploadC2CVoice(id, (it as VoiceResource).src) },
Video to { uploadC2CVideo(id, (it as VideoResource).src, it.thumb) }, Video to { uploadC2CVideo(id, (it as VideoResource).src, it.thumb) },
) )
) )
@ -83,6 +84,7 @@ internal object Transfer: FileTransfer() {
suspend fun uploadC2CPic( suspend fun uploadC2CPic(
peerId: String, peerId: String,
file: File, file: File,
record: MessageRecord? = null,
wait: Boolean = true wait: Boolean = true
): Boolean { ): Boolean {
return transC2CResource(peerId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_CAMERA, wait) { 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.mExtraObj = picUpExtraInfo
it.mIsPresend = true it.mIsPresend = true
it.delayShowProgressTimeInMs = 2000 it.delayShowProgressTimeInMs = 2000
it.mRec = record
} }
} }
suspend fun uploadGroupPic( suspend fun uploadGroupPic(
groupId: String, groupId: String,
file: File, file: File,
record: MessageRecord? = null,
wait: Boolean = true wait: Boolean = true
): Boolean { ): Boolean {
return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_CAMERA, wait) { return transTroopResource(groupId, file, FileMsg.TRANSFILE_TYPE_PIC, SEND_MSG_BUSINESS_TYPE_PIC_CAMERA, wait) {
val picUpExtraInfo = TransferRequest.PicUpExtraInfo() val picUpExtraInfo = TransferRequest.PicUpExtraInfo()
//picUpExtraInfo.mIsRaw = !TransfileHelper.isGifFile(file)
picUpExtraInfo.mIsRaw = false picUpExtraInfo.mIsRaw = false
picUpExtraInfo.mUinType = FileMsg.UIN_TROOP picUpExtraInfo.mUinType = FileMsg.UIN_TROOP
it.mPicSendSource = 8 it.mPicSendSource = 8
it.delayShowProgressTimeInMs = 2000 it.delayShowProgressTimeInMs = 2000
it.mExtraObj = picUpExtraInfo 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 kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import moe.fuqiuluo.qqinterface.servlet.MsgSvc import moe.fuqiuluo.qqinterface.servlet.MsgSvc
import moe.fuqiuluo.qqinterface.servlet.msg.maker.MessageElementMaker import moe.fuqiuluo.qqinterface.servlet.msg.maker.ElemMaker
import moe.fuqiuluo.qqinterface.servlet.msg.maker.MsgElementMaker import moe.fuqiuluo.qqinterface.servlet.msg.maker.NtMsgElementMaker
import moe.fuqiuluo.shamrock.helper.db.MessageDB import moe.fuqiuluo.shamrock.helper.db.MessageDB
import moe.fuqiuluo.shamrock.helper.db.MessageMapping import moe.fuqiuluo.shamrock.helper.db.MessageMapping
import moe.fuqiuluo.shamrock.remote.structures.SendMsgResult import moe.fuqiuluo.shamrock.remote.structures.SendMsgResult
@ -29,6 +29,7 @@ import moe.fuqiuluo.shamrock.tools.jsonArray
import protobuf.message.Elem import protobuf.message.Elem
import kotlin.coroutines.resume import kotlin.coroutines.resume
import kotlin.math.abs import kotlin.math.abs
import kotlin.time.Duration.Companion.seconds
internal object MessageHelper { internal object MessageHelper {
suspend fun sendMessageWithoutMsgId( suspend fun sendMessageWithoutMsgId(
@ -66,12 +67,15 @@ internal object MessageHelper {
suspend fun resendMsg(contact: Contact, msgId: Long, retryCnt: Int, msgHashId: Int): Result<SendMsgResult> { suspend fun resendMsg(contact: Contact, msgId: Long, retryCnt: Int, msgHashId: Int): Result<SendMsgResult> {
if (retryCnt < 0) return Result.failure(SendMsgException("消息发送超时次数过多")) if (retryCnt < 0) return Result.failure(SendMsgException("消息发送超时次数过多"))
val service = QRoute.api(IMsgService::class.java) val service = QRoute.api(IMsgService::class.java)
val result = withTimeoutOrNull(15000) { val result = withTimeoutOrNull(15.seconds) {
if (suspendCancellableCoroutine { val resendRet = suspendCancellableCoroutine {
service.resendMsg(contact, msgId) { result, _ -> service.resendMsg(contact, msgId) { result, _ ->
it.resume(result) it.resume(result)
} }
} != 0) { }
if (resendRet != 0 &&
resendRet != 4 // 使用OldBDH 100%触发
) {
resendMsg(contact, msgId, retryCnt - 1, msgHashId) resendMsg(contact, msgId, retryCnt - 1, msgHashId)
} else { } else {
Result.success(SendMsgResult(msgHashId, msgId, System.currentTimeMillis())) Result.success(SendMsgResult(msgHashId, msgId, System.currentTimeMillis()))
@ -287,18 +291,18 @@ internal object MessageHelper {
suspend fun messageArrayToMsgElements( suspend fun messageArrayToMsgElements(
chatType: Int, chatType: Int,
msgId: Long, msgId: Long,
targetUin: String, peerId: String,
messageList: JsonArray messageList: JsonArray
): Pair<Boolean, ArrayList<MsgElement>> { ): Pair<Boolean, ArrayList<MsgElement>> {
val msgList = arrayListOf<MsgElement>() val msgList = arrayListOf<MsgElement>()
var hasActionMsg = false var hasActionMsg = false
messageList.forEach { messageList.forEach {
val msg = it.jsonObject val msg = it.jsonObject
val maker = MsgElementMaker[msg["type"].asString] val maker = NtMsgElementMaker[msg["type"].asString]
if (maker != null) { if (maker != null) {
try { try {
val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject
maker(chatType, msgId, targetUin, data).onSuccess { msgElem -> maker(chatType, msgId, peerId, data).onSuccess { msgElem ->
msgList.add(msgElem) msgList.add(msgElem)
}.onFailure { }.onFailure {
if (it.javaClass != ActionMsgException::class.java) { if (it.javaClass != ActionMsgException::class.java) {
@ -321,18 +325,18 @@ internal object MessageHelper {
suspend fun messageArrayToMessageElements( suspend fun messageArrayToMessageElements(
chatType: Int, chatType: Int,
msgId: Long, msgId: Long,
targetUin: String, peerId: String,
messageList: JsonArray messageList: JsonArray
): Pair<Boolean, ArrayList<Elem>> { ): Pair<Boolean, ArrayList<Elem>> {
val msgList = arrayListOf<Elem>() val msgList = arrayListOf<Elem>()
var hasActionMsg = false var hasActionMsg = false
messageList.forEach { messageList.forEach {
val msg = it.jsonObject val msg = it.jsonObject
val maker = MessageElementMaker[msg["type"].asString] val maker = ElemMaker[msg["type"].asString]
if (maker != null) { if (maker != null) {
try { try {
val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject val data = msg["data"].asJsonObjectOrNull ?: EmptyJsonObject
maker(chatType, msgId, targetUin, data).onSuccess { msgElem -> maker(chatType, msgId, peerId, data).onSuccess { msgElem ->
msgList.add(msgElem) msgList.add(msgElem)
}.onFailure { }.onFailure {
if (it.javaClass != ActionMsgException::class.java) { if (it.javaClass != ActionMsgException::class.java) {
@ -427,11 +431,11 @@ internal object MessageHelper {
params[key] = value.json params[key] = value.json
} }
} }
val data = hashMapOf( val data = mapOf(
"type" to it["_type"]!!.json, "type" to it["_type"]!!.json,
"data" to JsonObject(params) "data" to JsonObject(params)
) )
arrayList.add(JsonObject(data)) arrayList.add(data.json)
} }
return arrayList.jsonArray return arrayList.jsonArray
} }

View File

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

View File

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

View File

@ -15,13 +15,13 @@ internal object SendGroupMessage: IActionHandler() {
return if (session.isString("message")) { return if (session.isString("message")) {
val autoEscape = session.getBooleanOrDefault("auto_escape", false) val autoEscape = session.getBooleanOrDefault("auto_escape", false)
val message = session.getString("message") 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")) { } else if (session.isObject("message")) {
val message = session.getObject("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 { } else {
val message = session.getArray("message") 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")) { return if (session.isString("message")) {
val autoEscape = session.getBooleanOrDefault("auto_escape", false) val autoEscape = session.getBooleanOrDefault("auto_escape", false)
val message = session.getString("message") 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")) { } else if (session.isArray("message")) {
val message = session.getArray("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 { } else {
val message = session.getObject("message") 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) { } catch (e: ParamsException) {
return noParam(e.message!!, session.echo) return noParam(e.message!!, session.echo)

View File

@ -1,10 +1,12 @@
package moe.fuqiuluo.shamrock.remote.action.handlers package moe.fuqiuluo.shamrock.remote.action.handlers
import kotlinx.atomicfu.atomic import kotlinx.atomicfu.atomic
import kotlinx.serialization.json.JsonElement
import moe.fuqiuluo.qqinterface.servlet.BaseSvc import moe.fuqiuluo.qqinterface.servlet.BaseSvc
import moe.fuqiuluo.shamrock.remote.action.ActionSession import moe.fuqiuluo.shamrock.remote.action.ActionSession
import moe.fuqiuluo.shamrock.remote.action.IActionHandler import moe.fuqiuluo.shamrock.remote.action.IActionHandler
import moe.fuqiuluo.shamrock.tools.EmptyJsonString
import moe.fuqiuluo.symbols.OneBotHandler import moe.fuqiuluo.symbols.OneBotHandler
import protobuf.auto.toByteArray import protobuf.auto.toByteArray
import protobuf.message.* import protobuf.message.*
@ -19,13 +21,19 @@ internal object SendMsgByResid : IActionHandler() {
private val msgSeq = atomic(1000) private val msgSeq = atomic(1000)
override suspend fun internalHandle(session: ActionSession): String { override suspend fun internalHandle(session: ActionSession): String {
val resid = session.getString("resid") val resId = session.getString("res_id")
val peerId = session.getString("peer") 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( val req = PbSendMsgReq(
routingHead = when (session.getStringOrNull("message_type")) { routingHead = when (messageType) {
"group" ->RoutingHead(grp = Grp(peerId.toUInt())) "group" -> RoutingHead(grp = Grp(peerId.toUInt()))
"private" ->RoutingHead( c2c = C2C(peerId.toUInt())) "private" -> RoutingHead(c2c = C2C(peerId.toUInt()))
else ->RoutingHead( grp = Grp(peerId.toUInt())) else -> RoutingHead(grp = Grp(peerId.toUInt()))
}, },
contentHead = ContentHead(1, 0, 0, 0), contentHead = ContentHead(1, 0, 0, 0),
msgBody = MsgBody( msgBody = MsgBody(
@ -34,7 +42,7 @@ internal object SendMsgByResid : IActionHandler() {
Elem( Elem(
generalFlags = GeneralFlags( generalFlags = GeneralFlags(
longTextFlag = 1u, longTextFlag = 1u,
longTextResid = resid.toByteArray() longTextResid = resId
) )
) )
) )
@ -45,6 +53,6 @@ internal object SendMsgByResid : IActionHandler() {
msgVia = 0u msgVia = 0u
) )
BaseSvc.sendBufferAW("MessageSvc.PbSendMsg", true, req.toByteArray()) 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, autoEscape = autoEscape,
echo = session.echo, echo = session.echo,
fromId = groupId?.toString() ?: userId.toString(), fromId = groupId?.toString() ?: userId.toString(),
retryCnt = retryCnt ?: 3, retryCnt = retryCnt ?: 5,
recallDuration = recallDuration recallDuration = recallDuration
) )
} else { } else {
@ -34,7 +34,7 @@ internal object SendPrivateMessage : IActionHandler() {
message = if (session.isArray("message")) session.getArray("message") else listOf(session.getObject("message")).jsonArray, message = if (session.isArray("message")) session.getArray("message") else listOf(session.getObject("message")).jsonArray,
echo = session.echo, echo = session.echo,
fromId = groupId?.toString() ?: userId.toString(), fromId = groupId?.toString() ?: userId.toString(),
retryCnt = retryCnt ?: 3, retryCnt = retryCnt ?: 5,
recallDuration = recallDuration recallDuration = recallDuration
) )
} }

View File

@ -1,59 +1,55 @@
package moe.fuqiuluo.shamrock.remote.api 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.Level
import moe.fuqiuluo.shamrock.helper.LogCenter 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() { fun Routing.testAction() {
if(ShamrockVersion.contains("dev")) { if (ShamrockConfig.isDev()) {
LogCenter.log("testAction is enabled.", Level.WARN) LogCenter.log("testAction is enabled.", Level.WARN)
} else { } else {
return return
} }
/* getOrPost("/send_msg_by_resid") {
get("/test/createUidFromTinyId") { val resId = fetchOrThrow("res_id")
val selfId = fetchOrThrow("selfId").toLong() val peerId = fetchOrThrow("peer_Id")
val peerId = fetchOrThrow("peerId").toLong() val messageType = fetchOrThrow("message_type")
call.respondText(NTServiceFetcher.kernelService.wrapperSession.msgService.createUidFromTinyId(selfId, peerId)) 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 msgService = NTServiceFetcher.kernelService.wrapperSession.msgService
val msgId = msgService.getMsgUniqueId(System.currentTimeMillis()) val msgId = msgService.getMsgUniqueId(System.currentTimeMillis())
msgService.addSendMsg(msgId, MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, TicketSvc.getUin()), arrayListOf( msgService.addSendMsg(msgId,
MsgElement().apply { MessageHelper.generateContact(MsgConstant.KCHATTYPEC2C, TicketSvc.getUin()),
elementType = MsgConstant.KELEMTYPETEXT arrayListOf(
textElement = TextElement().apply { MsgElement().apply {
content = "测试消息" 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)
} }
}) }
} ),
if (msgs == null) { hashMapOf())
call.respondText("failed") call.respondText("ok")
return@get }
}
call.respondText("msg -> " + msgs.map { it.toCQCode() }.joinToString("\n"))
}.onFailure {
call.respondText("failed: ${it.stackTraceToString()}")
return@get
}
}*/
} }

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
import moe.fuqiuluo.qqinterface.servlet.GroupSvc.requestGroupSystemMsgNew import moe.fuqiuluo.qqinterface.servlet.GroupSvc.requestGroupSystemMsgNew
import moe.fuqiuluo.qqinterface.servlet.TicketSvc.getLongUin import moe.fuqiuluo.qqinterface.servlet.TicketSvc.getLongUin
import moe.fuqiuluo.qqinterface.servlet.transfile.RichProtoSvc
import moe.fuqiuluo.shamrock.helper.ContactHelper import moe.fuqiuluo.shamrock.helper.ContactHelper
import moe.fuqiuluo.shamrock.helper.Level import moe.fuqiuluo.shamrock.helper.Level
import moe.fuqiuluo.shamrock.helper.LogCenter import moe.fuqiuluo.shamrock.helper.LogCenter
@ -30,11 +29,7 @@ import moe.fuqiuluo.symbols.decodeProtobuf
import protobuf.message.ContentHead import protobuf.message.ContentHead
import protobuf.message.MsgBody import protobuf.message.MsgBody
import protobuf.message.ResponseHead import protobuf.message.ResponseHead
import protobuf.message.multimedia.RichMediaForPicData
import protobuf.push.* import protobuf.push.*
import java.util.regex.Pattern
private val RKEY_PATTERN = Pattern.compile("rkey=([A-Za-z0-9_-]+)")
internal object PrimitiveListener { internal object PrimitiveListener {
fun registerListener() { fun registerListener() {
@ -94,22 +89,9 @@ internal object PrimitiveListener {
} }
private fun onGroupMessage(msgTime: Long, body: MsgBody) { private fun onGroupMessage(msgTime: Long, body: MsgBody) {
/*runCatching { 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
}
}
}
}*/
} }
private suspend fun onC2CPoke(msgTime: Long, body: MsgBody) { private suspend fun onC2CPoke(msgTime: Long, body: MsgBody) {

View File

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

View File

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