mirror of
https://github.com/yincongcyincong/wechat_chatter.git
synced 2026-07-15 10:26:52 +08:00
support send file
This commit is contained in:
+2
-1
@@ -12,4 +12,5 @@ release
|
||||
onebot/audio
|
||||
onebot/image
|
||||
onebot/log
|
||||
.comate
|
||||
.comate
|
||||
onebot/file
|
||||
+7
-3
@@ -37,7 +37,11 @@ const myPatterns = [
|
||||
pattern: "09 0F 46 F8 C9 01 00 B4 E8 03 18 AA 2A 21 40 B9"
|
||||
},
|
||||
{
|
||||
name: "downloadImagAddr",
|
||||
name: "downloadFileAddr",
|
||||
pattern: "68 52 41 F9 19 8D 41 F9 1A 00 14 8B 5F 03 19 EB"
|
||||
},
|
||||
{
|
||||
name: "downloadImageAddr",
|
||||
pattern: "68 52 41 F9 19 8D 41 F9 1A 00 14 8B 5F 03 19 EB"
|
||||
},
|
||||
{
|
||||
@@ -53,8 +57,8 @@ const myPatterns = [
|
||||
pattern: "3C 00 80 52 E0 C3 00 91"
|
||||
},
|
||||
{
|
||||
name: "hdPicDownloadAddr",
|
||||
pattern: "E0 63 00 91 41 00 80 52 25 5C 80 52 06 00 80 52"
|
||||
name: "startDownloadMedia",
|
||||
pattern: "B6 E3 02 D1 68 E1 01 D0 08 71 44 F9 08 01 40 F9 A8 83 1C F8 28 7C 42 39 09 1D 00 13 2A 48 40 F9 3F 01 00 71 48 B1 88 9A"
|
||||
}
|
||||
];
|
||||
|
||||
|
||||
+62
-3
@@ -6,6 +6,7 @@ if (!baseAddr) {
|
||||
var buf2RespAddr = baseAddr.add(0x3721FA0)
|
||||
var downloadImagAddr = baseAddr.add(0x4A6975C)
|
||||
var startDownloadMedia = baseAddr.add(0x494663C)
|
||||
var downloadFileAddr = baseAddr.add(0x4A084EC)
|
||||
|
||||
var downloadGlobalX0;
|
||||
var downloadFileX1 = ptr(0)
|
||||
@@ -140,14 +141,42 @@ function setReceiver() {
|
||||
|
||||
Interceptor.attach(startDownloadMedia, {
|
||||
onEnter: function (args) {
|
||||
if (downloadGlobalX0) {
|
||||
downloadGlobalX0 = this.context.x0;
|
||||
var fileIDAddr = this.context.x1.add(0x40).readPointer();
|
||||
var fileId = fileIDAddr?.readUtf8String();
|
||||
if (!fileId.endsWith("_1")) {
|
||||
return
|
||||
}
|
||||
downloadGlobalX0 = this.context.x0;
|
||||
console.log("downloadGlobalX0: " + downloadGlobalX0)
|
||||
|
||||
console.log(" [+] download file: ", fileId);
|
||||
const t = this.context.x1.add(0xA0).readU32()
|
||||
if (t === 3) {
|
||||
this.context.x1.add(0xA0).writeU32(0x02);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Interceptor.attach(downloadFileAddr, {
|
||||
onEnter: function (args) {
|
||||
var dataPtr = this.context.x1;
|
||||
var dataLen = this.context.x2.toInt32();
|
||||
var fileId = this.context.sp.add(0x30).readPointer().readUtf8String();
|
||||
var cdnUrl = this.context.x19.add(0x2F8).readPointer().readUtf8String();
|
||||
|
||||
if (dataLen > 0) {
|
||||
var buffer = dataPtr.readByteArray(dataLen);
|
||||
var uint8Array = new Uint8Array(buffer);
|
||||
|
||||
send({
|
||||
type: "download",
|
||||
media: Array.from(uint8Array),
|
||||
file_id: fileId,
|
||||
cdn_url: cdnUrl,
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Interceptor.attach(downloadImagAddr, {
|
||||
onEnter: function (args) {
|
||||
var dataPtr = this.context.x1;
|
||||
@@ -186,6 +215,21 @@ function getMessages(content, sender, mediaContent) {
|
||||
messages.push({type: "image", data: {text: part}});
|
||||
} else if (part.startsWith("<msg><voicemsg")) {
|
||||
messages.push({type: "record", data: {text: part}});
|
||||
} else if (part.startsWith("<?xml version=\"1.0\"?><msg><appmsg")) {
|
||||
const regex = /<type>(.*?)<\/type>/s;
|
||||
const match = part.match(regex);
|
||||
if (match.length > 1) {
|
||||
switch (match[1]) {
|
||||
case "5":
|
||||
messages.push({type: "share", data: {text: part}});
|
||||
break
|
||||
case "6":
|
||||
messages.push({type: "file", data: {text: part}});
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if (content.startsWith("<msg><emoji")) {
|
||||
messages.push({type: "face", data: {text: part}});
|
||||
} else {
|
||||
messages.push({type: "text", data: {text: part}});
|
||||
}
|
||||
@@ -199,6 +243,21 @@ function getMessages(content, sender, mediaContent) {
|
||||
mediaContent = mediaContent.subarray(audioStart);
|
||||
}
|
||||
messages.push({type: "record", data: {text: content, media: Array.from(mediaContent)}});
|
||||
} else if (content.startsWith("<?xml version=\"1.0\"?><msg><appmsg")) {
|
||||
const regex = /<type>(.*?)<\/type>/s;
|
||||
const match = content.match(regex);
|
||||
if (match.length > 1) {
|
||||
switch (match[1]) {
|
||||
case "5":
|
||||
messages.push({type: "share", data: {text: content}});
|
||||
break
|
||||
case "6":
|
||||
messages.push({type: "file", data: {text: content}});
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if (content.startsWith("<msg><emoji")) {
|
||||
messages.push({type: "face", data: {text: content}});
|
||||
} else {
|
||||
messages.push({type: "text", data: {text: content}});
|
||||
}
|
||||
|
||||
+1
-1
@@ -454,7 +454,7 @@ setImmediate(AttachSendTextProto);
|
||||
// 拦截 SendTextProto 编码逻辑,注入自定义 Payload
|
||||
function attachSendTextProto() {
|
||||
console.log("[+] proto注入拦截目标地址: " + protobufAddr);
|
||||
textProtoX1PayloadAddr = Memory.alloc(1024);
|
||||
textProtoX1PayloadAddr = Memory.alloc(3096);
|
||||
console.log("[+] Frida 分配的 Payload 地址: " + textProtoX1PayloadAddr);
|
||||
|
||||
Interceptor.attach(protobufAddr, {
|
||||
|
||||
+1
-1
@@ -441,7 +441,7 @@ function triggerSendTextMessage(taskId, receiver, content, atUser) {
|
||||
// 拦截 SendTextProto 编码逻辑,注入自定义 Payload
|
||||
function attachSendTextProto() {
|
||||
console.log("[+] proto注入拦截目标地址: " + protobufAddr);
|
||||
textProtoX1PayloadAddr = Memory.alloc(1024);
|
||||
textProtoX1PayloadAddr = Memory.alloc(3096);
|
||||
console.log("[+] Frida 分配的 Payload 地址: " + textProtoX1PayloadAddr);
|
||||
|
||||
Interceptor.attach(protobufAddr, {
|
||||
|
||||
+1
-1
@@ -321,7 +321,7 @@ setImmediate(AttachSendTextProto);
|
||||
// 拦截 SendTextProto 编码逻辑,注入自定义 Payload
|
||||
function attachSendTextProto() {
|
||||
console.log("[+] proto注入拦截目标地址: " + protobufAddr);
|
||||
textProtoX1PayloadAddr = Memory.alloc(1024);
|
||||
textProtoX1PayloadAddr = Memory.alloc(3096);
|
||||
console.log("[+] Frida 分配的 Payload 地址: " + textProtoX1PayloadAddr);
|
||||
|
||||
Interceptor.attach(protobufAddr, {
|
||||
|
||||
@@ -72,15 +72,6 @@ func initFlag() {
|
||||
fmt.Println("ConnType", config.ConnType)
|
||||
fmt.Println("SendInterval", config.SendInterval)
|
||||
fmt.Println("LogLevel", logLevel)
|
||||
|
||||
err := EnsureDir("./audio")
|
||||
if err != nil {
|
||||
Fatal("❌ 无法创建音频目录", err)
|
||||
}
|
||||
err = EnsureDir("./image")
|
||||
if err != nil {
|
||||
Fatal("❌ 无法创建图片目录", err)
|
||||
}
|
||||
}
|
||||
|
||||
func initFridaGadget() {
|
||||
|
||||
+4
-1
@@ -13,9 +13,12 @@ func Download(rawMsg []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
Info("下载文件", "file_id", downloadReq.FileID, "media_len", len(downloadReq.Media), "cdn_url", downloadReq.CDNURL[:30])
|
||||
Info("下载文件", "file_id", downloadReq.FileID, "media_len", len(downloadReq.Media), "cdn_url", downloadReq.CDNURL)
|
||||
if downloadReqInter, ok := userID2FileMsgMap.Load(downloadReq.CDNURL); ok {
|
||||
beforeDownloadReq := downloadReqInter.(*DownloadRequest)
|
||||
if beforeDownloadReq.FilePath != "" {
|
||||
return nil
|
||||
}
|
||||
beforeDownloadReq.Media = append(beforeDownloadReq.Media, downloadReq.Media...)
|
||||
beforeDownloadReq.LastAppendTime = time.Now().UnixMilli()
|
||||
} else {
|
||||
|
||||
+93
-2
@@ -49,6 +49,12 @@ type SendMsg struct {
|
||||
Content string
|
||||
Type string
|
||||
AtUser string
|
||||
|
||||
FIleCdnUrl string
|
||||
Md5 string
|
||||
AesKey string
|
||||
FilePath string
|
||||
FileType int
|
||||
}
|
||||
|
||||
// SendRequest 请求结构体
|
||||
@@ -109,8 +115,16 @@ type VoiceMsgInfo struct {
|
||||
|
||||
// FileMsg 对应 <msg> 标签
|
||||
type FileMsg struct {
|
||||
XMLName xml.Name `xml:"msg"`
|
||||
Image Image `xml:"img"`
|
||||
XMLName xml.Name `xml:"msg"`
|
||||
Image Image `xml:"img"`
|
||||
Emoji Emoji `xml:"emoji"`
|
||||
GameExt GameExt `xml:"gameext"`
|
||||
AppMsg AppMsg `xml:"appmsg"`
|
||||
ExtCommonInfo ExtCommonInfo `xml:"extcommoninfo"`
|
||||
FromUsername string `xml:"fromusername"`
|
||||
Scene string `xml:"scene"`
|
||||
AppInfo AppInfo `xml:"appinfo"`
|
||||
CommentURL string `xml:"commenturl"`
|
||||
}
|
||||
|
||||
// Image 对应 <img> 标签及其属性和子节点
|
||||
@@ -143,9 +157,86 @@ type DownloadRequest struct {
|
||||
Media []byte `json:"media"`
|
||||
CDNURL string `json:"cdn_url"`
|
||||
LastAppendTime int64 `json:"last_append_time"`
|
||||
FilePath string `json:"file_path"`
|
||||
}
|
||||
|
||||
type ScriptMessage struct {
|
||||
Type string `json:"type"`
|
||||
Payload interface{} `json:"payload"`
|
||||
}
|
||||
|
||||
type Emoji struct {
|
||||
FromUsername string `xml:"fromusername,attr"`
|
||||
ToUserName string `xml:"tousername,attr"`
|
||||
Type string `xml:"type,attr"`
|
||||
IdBuffer string `xml:"idbuffer,attr"`
|
||||
Md5 string `xml:"md5,attr"`
|
||||
Len string `xml:"len,attr"`
|
||||
ProductId string `xml:"productid,attr"`
|
||||
AndroidMd5 string `xml:"androidmd5,attr"`
|
||||
AndroidLen string `xml:"androidlen,attr"`
|
||||
S60v3Md5 string `xml:"s60v3md5,attr"`
|
||||
S60v3Len string `xml:"s60v3len,attr"`
|
||||
S60v5Md5 string `xml:"s60v5md5,attr"`
|
||||
S60v5Len string `xml:"s60v5len,attr"`
|
||||
CdnUrl string `xml:"cdnurl,attr"`
|
||||
DesignerId string `xml:"designerid,attr"`
|
||||
ThumbUrl string `xml:"thumburl,attr"`
|
||||
EncryptUrl string `xml:"encrypturl,attr"`
|
||||
AesKey string `xml:"aeskey,attr"`
|
||||
ExternUrl string `xml:"externurl,attr"`
|
||||
ExternMd5 string `xml:"externmd5,attr"`
|
||||
Width string `xml:"width,attr"`
|
||||
Height string `xml:"height,attr"`
|
||||
TpUrl string `xml:"tpurl,attr"`
|
||||
TpAuthKey string `xml:"tpauthkey,attr"`
|
||||
AttachedText string `xml:"attachedtext,attr"`
|
||||
AttachedTextColor string `xml:"attachedtextcolor,attr"`
|
||||
LensId string `xml:"lensid,attr"`
|
||||
EmojiAttr string `xml:"emojiattr,attr"`
|
||||
LinkId string `xml:"linkid,attr"`
|
||||
Desc string `xml:"desc,attr"`
|
||||
}
|
||||
|
||||
type GameExt struct {
|
||||
Type string `xml:"type,attr"`
|
||||
Content string `xml:"content,attr"`
|
||||
}
|
||||
|
||||
type AppMsg struct {
|
||||
AppID string `xml:"appid,attr"`
|
||||
SDKVer string `xml:"sdkver,attr"`
|
||||
Title string `xml:"title"`
|
||||
Type string `xml:"type"`
|
||||
Action string `xml:"action"`
|
||||
AppAttach AppAttach `xml:"appattach"`
|
||||
MD5 string `xml:"md5"`
|
||||
WebViewShared WebViewShared `xml:"webviewshared"`
|
||||
}
|
||||
|
||||
type AppAttach struct {
|
||||
TotalLen string `xml:"totallen"`
|
||||
FileExt string `xml:"fileext"`
|
||||
AttachID string `xml:"attachid"`
|
||||
CdnAttachURL string `xml:"cdnattachurl"`
|
||||
CdnThumbAesKey string `xml:"cdnthumbaeskey"`
|
||||
AesKey string `xml:"aeskey"`
|
||||
EncryVer string `xml:"encryver"`
|
||||
FileKey string `xml:"filekey"`
|
||||
OverwriteNewMsgID string `xml:"overwrite_newmsgid"`
|
||||
FileUploadToken string `xml:"fileuploadtoken"`
|
||||
}
|
||||
|
||||
type WebViewShared struct {
|
||||
JsAppID string `xml:"jsAppId"`
|
||||
PublisherReqID string `xml:"publisherReqId"`
|
||||
}
|
||||
|
||||
type ExtCommonInfo struct {
|
||||
MediaExpireAt string `xml:"media_expire_at"`
|
||||
}
|
||||
|
||||
type AppInfo struct {
|
||||
Version string `xml:"version"`
|
||||
AppName string `xml:"appname"`
|
||||
}
|
||||
|
||||
+237
-9
@@ -299,8 +299,16 @@ var uploadGetCallbackWrapperFuncAddr = baseAddr.add({{.uploadGetCallbackWrapperF
|
||||
var uploadOnCompleteAddr = baseAddr.add({{.uploadOnCompleteAddr}});
|
||||
var uploadOnCompleteFuncAddr = baseAddr.add({{.uploadOnCompleteFuncAddr}});
|
||||
var downloadImagAddr = baseAddr.add({{.downloadImagAddr}});
|
||||
var hdPicDownloadAddr = baseAddr.add({{.hdPicDownloadAddr}})
|
||||
var startDownloadMedia = baseAddr.add({{.startDownloadMedia}})
|
||||
var downloadFileAddr = baseAddr.add({{.downloadFileAddr}})
|
||||
|
||||
var downloadGlobalX0;
|
||||
var downloadFileX1 = ptr(0)
|
||||
var fileIdAddr = ptr(0)
|
||||
var fileMd5Addr = ptr(0)
|
||||
var downloadAesKeyAddr = ptr(0)
|
||||
var filePathAddr = ptr(0)
|
||||
var fileCdnUrlAddr = ptr(0)
|
||||
var uploadImageX1 = ptr(0);
|
||||
var imgCgiAddr = ptr(0);
|
||||
var sendImgMessageAddr = ptr(0);
|
||||
@@ -538,7 +546,7 @@ setImmediate(AttachSendTextProto);
|
||||
|
||||
// 拦截 SendTextProto 编码逻辑,注入自定义 Payload
|
||||
function attachSendTextProto() {
|
||||
textProtoX1PayloadAddr = Memory.alloc(2048);
|
||||
textProtoX1PayloadAddr = Memory.alloc(3096);
|
||||
console.log("[+] Frida 分配的 Payload 地址: " + textProtoX1PayloadAddr);
|
||||
|
||||
Interceptor.attach(protobufAddr, {
|
||||
@@ -1171,12 +1179,25 @@ setImmediate(attachGetCallbackFromWrapper);
|
||||
rpc.exports = {
|
||||
triggerSendImgMessage: triggerSendImgMessage,
|
||||
triggerUploadImg: triggerUploadImg,
|
||||
triggerSendTextMessage: triggerSendTextMessage
|
||||
triggerSendTextMessage: triggerSendTextMessage,
|
||||
triggerDownload: triggerDownload,
|
||||
};
|
||||
|
||||
// -------------------------发送图片消息分区-------------------------
|
||||
|
||||
// -------------------------接收消息分区-------------------------
|
||||
function setupDownloadFileDynamic() {
|
||||
downloadFileX1 = Memory.alloc(1624)
|
||||
fileIdAddr = Memory.alloc(128)
|
||||
fileMd5Addr = Memory.alloc(128)
|
||||
downloadAesKeyAddr = Memory.alloc(128)
|
||||
filePathAddr = Memory.alloc(256)
|
||||
fileCdnUrlAddr = Memory.alloc(256)
|
||||
|
||||
}
|
||||
|
||||
setImmediate(setupDownloadFileDynamic)
|
||||
|
||||
function setReceiver() {
|
||||
Interceptor.attach(buf2RespAddr, {
|
||||
onEnter: function (args) {
|
||||
@@ -1287,8 +1308,9 @@ function setReceiver() {
|
||||
},
|
||||
});
|
||||
|
||||
Interceptor.attach(hdPicDownloadAddr, {
|
||||
Interceptor.attach(startDownloadMedia, {
|
||||
onEnter: function (args) {
|
||||
downloadGlobalX0 = this.context.x0;
|
||||
var fileIDAddr = this.context.x1.add(0x40).readPointer();
|
||||
var fileId = fileIDAddr?.readUtf8String();
|
||||
if (!fileId.endsWith("_1")) {
|
||||
@@ -1296,12 +1318,35 @@ function setReceiver() {
|
||||
}
|
||||
|
||||
console.log(" [+] download file: ", fileId);
|
||||
this.context.x1.add(0x148).writeByteArray([0xa0, 0x86, 0x01, 0x00]);
|
||||
this.context.x1.add(0xA0).writeU32(0x02);
|
||||
const t = this.context.x1.add(0xA0).readU32()
|
||||
if (t === 3) {
|
||||
this.context.x1.add(0xA0).writeU32(0x02);
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
Interceptor.attach(downloadFileAddr, {
|
||||
onEnter: function (args) {
|
||||
var dataPtr = this.context.x1;
|
||||
var dataLen = this.context.x2.toInt32();
|
||||
var fileId = this.context.sp.add(0x30).readPointer().readUtf8String();
|
||||
var cdnUrl = this.context.x19.add(0x2F8).readPointer().readUtf8String();
|
||||
|
||||
if (dataLen > 0) {
|
||||
var buffer = dataPtr.readByteArray(dataLen);
|
||||
var uint8Array = new Uint8Array(buffer);
|
||||
|
||||
send({
|
||||
type: "download",
|
||||
media: Array.from(uint8Array),
|
||||
file_id: fileId,
|
||||
cdn_url: cdnUrl,
|
||||
})
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Interceptor.attach(downloadImagAddr, { // 建议使用函数起始地址或你计算出的偏移地址
|
||||
Interceptor.attach(downloadImagAddr, {
|
||||
onEnter: function (args) {
|
||||
var dataPtr = this.context.x1;
|
||||
var dataLen = this.context.x2.toInt32();
|
||||
@@ -1323,10 +1368,163 @@ function setReceiver() {
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// 使用 setImmediate 确保在模块加载后执行
|
||||
setImmediate(setReceiver)
|
||||
|
||||
// fileType: HdImage => 1,Image => 2, thumbImage => 3, Video => 4, File => 5,
|
||||
function triggerDownload(receiver, cdnUrl, aesKey, filePath, fileType) {
|
||||
const downloadMediaPayload = [
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x00
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x10
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x20
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x30
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xF0, 0xB6, 0x4C, 0xFC, 0x0A, 0x00, 0x00, 0x00, // 0x40
|
||||
0x24, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80,
|
||||
0x80, 0x10, 0x4B, 0xFA, 0x0A, 0x00, 0x00, 0x00, // 0x58
|
||||
0xB2, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80,
|
||||
0xF0, 0xB3, 0x4C, 0xFC, 0x0A, 0x00, 0x00, 0x00, // 0x70
|
||||
0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x28, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80,
|
||||
0x60, 0xC4, 0x2D, 0xFE, 0x0A, 0x00, 0x00, 0x00, // 0x88
|
||||
0xC8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x90
|
||||
0xD0, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, // 0x98
|
||||
0x03, 0x00, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF, // 0xa0
|
||||
0x00, 0x00, 0x00, 0x00, 0x01, 0xAA, 0xAA, 0xAA, // 0xa8
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0xb0
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0xc0
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0xd0
|
||||
0x01, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0xd8
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0xe0
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0xf0
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x100
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x110
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x02, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, // 0x128
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x11, 0x28, 0x28, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x148
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x02, 0x00, 0x00, 0xAA, 0xAA, 0xAA, // 0x170
|
||||
0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, // 0x180
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x1E, 0x00, 0x00, 0x00, 0xAA, 0xAA, 0xAA, 0xAA, // 0x1a0
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0xAA, 0xAA, 0xAA, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x22, 0x1A, 0xFE, 0x0A, 0x00, 0x00, 0x00, // 0x1d0
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x1f0
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x200
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x288
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x298
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x2a0
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
|
||||
0x00, 0x4F, 0x56, 0xFC, 0x0A, 0x00, 0x00, 0x00, // 0x2c0
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x300
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, // 0x318
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // 0x340
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x01, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00, // 0x378
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x03, 0x00, 0x00, 0x00, 0x0A, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x80, 0x3F, 0x00, 0x00, 0x00, 0x00, // 0x3e0
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
];
|
||||
|
||||
patchString(fileIdAddr, receiver + "_" + String(Math.floor(Date.now() / 1000)) + "_" + Math.floor(Math.random() * 1001) + "_1");
|
||||
patchString(fileCdnUrlAddr, cdnUrl)
|
||||
patchString(downloadAesKeyAddr, aesKey)
|
||||
patchString(filePathAddr, filePath);
|
||||
|
||||
downloadFileX1.writeByteArray(downloadMediaPayload);
|
||||
downloadFileX1.add(0x40).writePointer(fileIdAddr);
|
||||
downloadFileX1.add(0x58).writePointer(fileCdnUrlAddr);
|
||||
downloadFileX1.add(0x70).writePointer(downloadAesKeyAddr);
|
||||
downloadFileX1.add(0x88).writePointer(filePathAddr);
|
||||
downloadFileX1.add(0xa0).writeU32(fileType);
|
||||
|
||||
const startDwMedia = new NativeFunction(startDownloadMedia, 'int64', ['pointer', 'pointer']);
|
||||
const result = startDwMedia(downloadGlobalX0, downloadFileX1);
|
||||
|
||||
console.log("下载调用结果: " + result);
|
||||
return result;
|
||||
}
|
||||
|
||||
function getMessages(content, sender, mediaContent) {
|
||||
var messages = [];
|
||||
if (sender.includes("@chatroom")) {
|
||||
@@ -1339,6 +1537,21 @@ function getMessages(content, sender, mediaContent) {
|
||||
messages.push({type: "image", data: {text: part}});
|
||||
} else if (part.startsWith("<msg><voicemsg")) {
|
||||
messages.push({type: "record", data: {text: part}});
|
||||
} else if (part.startsWith("<?xml version=\"1.0\"?><msg><appmsg")) {
|
||||
const regex = /<type>(.*?)<\/type>/s;
|
||||
const match = part.match(regex);
|
||||
if (match.length > 1) {
|
||||
switch (match[1]) {
|
||||
case "5":
|
||||
messages.push({type: "share", data: {text: part}});
|
||||
break
|
||||
case "6":
|
||||
messages.push({type: "file", data: {text: part}});
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if (content.startsWith("<msg><emoji")) {
|
||||
messages.push({type: "face", data: {text: part}});
|
||||
} else {
|
||||
messages.push({type: "text", data: {text: part}});
|
||||
}
|
||||
@@ -1352,6 +1565,21 @@ function getMessages(content, sender, mediaContent) {
|
||||
mediaContent = mediaContent.subarray(audioStart);
|
||||
}
|
||||
messages.push({type: "record", data: {text: content, media: Array.from(mediaContent)}});
|
||||
} else if (content.startsWith("<?xml version=\"1.0\"?><msg><appmsg")) {
|
||||
const regex = /<type>(.*?)<\/type>/s;
|
||||
const match = content.match(regex);
|
||||
if (match.length > 1) {
|
||||
switch (match[1]) {
|
||||
case "5":
|
||||
messages.push({type: "share", data: {text: content}});
|
||||
break
|
||||
case "6":
|
||||
messages.push({type: "file", data: {text: content}});
|
||||
break
|
||||
}
|
||||
}
|
||||
} else if (content.startsWith("<msg><emoji")) {
|
||||
messages.push({type: "face", data: {text: content}});
|
||||
} else {
|
||||
messages.push({type: "text", data: {text: content}});
|
||||
}
|
||||
|
||||
+171
-99
@@ -6,11 +6,12 @@ import (
|
||||
"crypto/md5"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -39,7 +40,7 @@ func SaveBase64Image(base64Data string) (string, string, error) {
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
randomNumber := r.Intn(1000) // 生成 0-999 的随机数
|
||||
timestamp := time.Now().Unix()
|
||||
fileName := fmt.Sprintf("%d_%d.%s", randomNumber, timestamp, DetectImageFormat(data))
|
||||
fileName := fmt.Sprintf("%d_%d.%s", randomNumber, timestamp, DetectFileFormat(data))
|
||||
targetPath := config.ImagePath + fileName
|
||||
dir := filepath.Dir(targetPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
@@ -73,36 +74,6 @@ func GetFileMD5(filePath string) (string, error) {
|
||||
return hex.EncodeToString(hash.Sum(nil)), nil
|
||||
}
|
||||
|
||||
func DetectImageFormat(data []byte) string {
|
||||
if len(data) < 12 {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
switch {
|
||||
case bytes.HasPrefix(data, []byte{0xFF, 0xD8, 0xFF}):
|
||||
return "jpg"
|
||||
case bytes.HasPrefix(data, []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}):
|
||||
return "png"
|
||||
case bytes.HasPrefix(data, []byte("GIF87a")) || bytes.HasPrefix(data, []byte("GIF89a")):
|
||||
return "gif"
|
||||
case bytes.HasPrefix(data, []byte{0x42, 0x4D}):
|
||||
return "bmp"
|
||||
case bytes.HasPrefix(data, []byte("RIFF")) && bytes.HasPrefix(data[8:], []byte("WEBP")):
|
||||
return "webp"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
func EnsureDir(path string) error {
|
||||
_, err := os.Stat(path)
|
||||
if os.IsNotExist(err) {
|
||||
return os.MkdirAll(path, 0755)
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func SaveAudioFile(silkBytes []byte) (path string, err error) {
|
||||
mp3Bytes, err := SilkToMp3(silkBytes)
|
||||
if err != nil {
|
||||
@@ -157,63 +128,7 @@ func SilkToMp3(silkBytes []byte) ([]byte, error) {
|
||||
return out.Bytes(), nil
|
||||
}
|
||||
|
||||
func HandleMsg(jsonData []byte) ([]byte, error) {
|
||||
m := new(WechatMessage)
|
||||
err := json.Unmarshal(jsonData, m)
|
||||
if err != nil {
|
||||
Error("解析消息失败", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
myWechatId = m.SelfID
|
||||
if m.GroupId != "" {
|
||||
userID2NicknameMap.Store(m.GroupId+"_"+m.UserID, m.Sender.Nickname)
|
||||
}
|
||||
|
||||
for _, msg := range m.Message {
|
||||
if msg.Type == "record" {
|
||||
path, err := SaveAudioFile(msg.Data.Media)
|
||||
if err != nil {
|
||||
Error("保存音频失败", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
msg.Data.URL = "file://" + path
|
||||
msg.Data.Media = nil
|
||||
} else if msg.Type == "image" {
|
||||
var fileMsg FileMsg
|
||||
err = xml.Unmarshal([]byte(msg.Data.Text), &fileMsg)
|
||||
if err != nil {
|
||||
Error("XML解析失败", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 如果有图片等待图片下载完成再处理
|
||||
if len(msg.Data.Media) == 0 {
|
||||
for i := 0; i < 3; i++ {
|
||||
if downloadMsgInter, ok := userID2FileMsgMap.Load(fileMsg.Image.ThumbURL); ok {
|
||||
downloadReq := downloadMsgInter.(*DownloadRequest)
|
||||
// 进行重试,最后一次进行兜底
|
||||
if time.Now().UnixMilli()-downloadReq.LastAppendTime < 500 && i < 2 {
|
||||
Info("等待图片下载完成", "times", i, "url", fileMsg.Image.ThumbURL)
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
}
|
||||
aesKey, _ := hex.DecodeString(fileMsg.Image.AesKey)
|
||||
path, err := GetImagePath(downloadReq.Media, aesKey)
|
||||
if err != nil {
|
||||
Error("获取图片路径失败", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
msg.Data.URL = "file://" + path
|
||||
userID2FileMsgMap.Delete(fileMsg.Image.ThumbURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
func GetImagePath(data []byte, key []byte) (string, error) {
|
||||
func GetFilePath(data []byte, key []byte) (string, error) {
|
||||
block, _ := aes.NewCipher(key)
|
||||
decrypted := make([]byte, len(data))
|
||||
bs := block.BlockSize()
|
||||
@@ -221,17 +136,120 @@ func GetImagePath(data []byte, key []byte) (string, error) {
|
||||
block.Decrypt(decrypted[i:i+bs], data[i:i+bs])
|
||||
}
|
||||
|
||||
if bytes.HasPrefix(decrypted, []byte{0xFF, 0xD8, 0xFF}) {
|
||||
return SaveImageToFile("jpg", decrypted)
|
||||
} else if bytes.HasPrefix(decrypted, []byte{0x89, 0x50, 0x4E, 0x47}) {
|
||||
return SaveImageToFile("png", decrypted)
|
||||
} else if bytes.HasPrefix(decrypted, []byte("GIF89a")) || bytes.HasPrefix(decrypted, []byte("GIF87a")) {
|
||||
return SaveImageToFile("gif", decrypted)
|
||||
} else if bytes.HasPrefix(decrypted, []byte{0x42, 0x4D}) {
|
||||
return SaveImageToFile("bmp", decrypted)
|
||||
ext := DetectFileFormat(decrypted)
|
||||
if ext == "unknown" {
|
||||
return "", fmt.Errorf("无法解析的文件数据")
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("无法解析的图像数据")
|
||||
return SaveFileToFile(ext, decrypted)
|
||||
}
|
||||
|
||||
// DetectFileFormat 检测文件格式,返回扩展名
|
||||
func DetectFileFormat(data []byte) string {
|
||||
if len(data) < 8 {
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
switch {
|
||||
// 图片格式
|
||||
case bytes.HasPrefix(data, []byte{0xFF, 0xD8, 0xFF}):
|
||||
return "jpg"
|
||||
case bytes.HasPrefix(data, []byte{0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A}):
|
||||
return "png"
|
||||
case bytes.HasPrefix(data, []byte("GIF87a")) || bytes.HasPrefix(data, []byte("GIF89a")):
|
||||
return "gif"
|
||||
case bytes.HasPrefix(data, []byte{0x42, 0x4D}):
|
||||
return "bmp"
|
||||
case bytes.HasPrefix(data, []byte("RIFF")) && len(data) > 8 && bytes.HasPrefix(data[8:], []byte("WEBP")):
|
||||
return "webp"
|
||||
|
||||
// 文档格式
|
||||
case bytes.HasPrefix(data, []byte("%PDF")):
|
||||
return "pdf"
|
||||
|
||||
// Office 2007+ 格式 (docx, xlsx, pptx 都是 ZIP 格式)
|
||||
case bytes.HasPrefix(data, []byte{0x50, 0x4B, 0x03, 0x04}):
|
||||
return detectOfficeFormat(data)
|
||||
|
||||
// Office 97-2003 格式 (OLE2 格式)
|
||||
case bytes.HasPrefix(data, []byte{0xD0, 0xCF, 0x11, 0xE0, 0xA1, 0xB1, 0x1A, 0xE1}):
|
||||
return detectLegacyOfficeFormat(data)
|
||||
|
||||
// 压缩文件
|
||||
case bytes.HasPrefix(data, []byte("Rar!\x1a\x07")):
|
||||
return "rar"
|
||||
case bytes.HasPrefix(data, []byte("7z\xBC\xAF\x27\x1C")):
|
||||
return "7z"
|
||||
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
|
||||
// detectOfficeFormat 检测 Office 2007+ 文件具体类型
|
||||
func detectOfficeFormat(data []byte) string {
|
||||
// 查找 ZIP 内的特定文件来区分类型
|
||||
if bytes.Contains(data, []byte("[Content_Types].xml")) {
|
||||
if bytes.Contains(data, []byte("word/")) {
|
||||
return "docx"
|
||||
}
|
||||
if bytes.Contains(data, []byte("xl/")) {
|
||||
return "xlsx"
|
||||
}
|
||||
if bytes.Contains(data, []byte("ppt/")) {
|
||||
return "pptx"
|
||||
}
|
||||
}
|
||||
// 普通 ZIP 文件
|
||||
return "zip"
|
||||
}
|
||||
|
||||
// detectLegacyOfficeFormat 检测 Office 97-2003 文件具体类型
|
||||
func detectLegacyOfficeFormat(data []byte) string {
|
||||
// 通过文件内容特征判断
|
||||
if bytes.Contains(data, []byte("Word.Document")) {
|
||||
return "doc"
|
||||
}
|
||||
if bytes.Contains(data, []byte("Excel.Sheet")) {
|
||||
return "xls"
|
||||
}
|
||||
if bytes.Contains(data, []byte("PowerPoint.Show")) {
|
||||
return "ppt"
|
||||
}
|
||||
return "ole"
|
||||
}
|
||||
|
||||
// SaveFileToFile 通用文件保存函数
|
||||
func SaveFileToFile(ext string, data []byte) (string, error) {
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
randomNumber := r.Intn(1000)
|
||||
timestamp := time.Now().Unix()
|
||||
|
||||
// 根据文件类型选择保存目录
|
||||
dir := "file"
|
||||
if ext == "jpg" || ext == "png" || ext == "gif" || ext == "bmp" || ext == "webp" {
|
||||
dir = "image"
|
||||
}
|
||||
|
||||
fileName := fmt.Sprintf("%d_%d.%s", randomNumber, timestamp, ext)
|
||||
targetPath := filepath.Dir(exePath) + "/" + dir + "/" + fileName
|
||||
|
||||
// 确保目录存在
|
||||
if err := os.MkdirAll(filepath.Dir(targetPath), 0755); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
err = os.WriteFile(targetPath, data, 0644)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return targetPath, nil
|
||||
}
|
||||
|
||||
func SaveImageToFile(ext string, data []byte) (string, error) {
|
||||
@@ -262,3 +280,57 @@ func GetWeChatPID() (int, error) {
|
||||
|
||||
return strconv.Atoi(strings.TrimSpace(string(output)))
|
||||
}
|
||||
|
||||
func DownloadFile(urlStr string) ([]byte, error) {
|
||||
if urlStr == "" {
|
||||
return nil, errors.New("url is empty")
|
||||
}
|
||||
|
||||
// 解析 URL 以判断协议
|
||||
parsedURL, err := url.Parse(urlStr)
|
||||
if err != nil {
|
||||
return nil, errors.New("invalid URL format: " + err.Error())
|
||||
}
|
||||
|
||||
// 处理 file:// 协议
|
||||
if parsedURL.Scheme == "file" {
|
||||
// 去除 "file://" 前缀,得到本地文件路径
|
||||
filePath := strings.TrimPrefix(urlStr, "file://")
|
||||
// 对于 Windows 路径可能需要额外处理,但你的路径是 macOS/Linux 格式
|
||||
data, err := os.ReadFile(filePath)
|
||||
if err != nil {
|
||||
return nil, errors.New("failed to read local file: " + err.Error())
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
||||
client := &http.Client{}
|
||||
resp, err := client.Get(urlStr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, errors.New("failed to download file: " + resp.Status)
|
||||
}
|
||||
|
||||
data, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return data, nil
|
||||
}
|
||||
|
||||
// DetectAndSaveImage 自动检测图片格式并保存到本地
|
||||
func DetectAndSaveImage(data []byte) (string, error) {
|
||||
// 先检测图片格式
|
||||
ext := DetectFileFormat(data)
|
||||
if ext == "unknown" {
|
||||
return "", fmt.Errorf("无法识别的图片格式")
|
||||
}
|
||||
|
||||
// 调用保存函数
|
||||
return SaveImageToFile(ext, data)
|
||||
}
|
||||
|
||||
+136
-2
@@ -2,6 +2,10 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"runtime/debug"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
@@ -41,10 +45,15 @@ func SendWechatMsg(m *SendMsg) {
|
||||
targetId = m.GroupID
|
||||
}
|
||||
|
||||
if targetId == "" {
|
||||
Error("目标为空", "task_id", currTaskId, "target_id", targetId)
|
||||
return
|
||||
}
|
||||
|
||||
switch m.Type {
|
||||
case "text":
|
||||
result := fridaScript.ExportsCall("triggerSendTextMessage", currTaskId, targetId, m.Content, m.AtUser)
|
||||
Info("📩 发送文本任务执行结果", "result", result, "task_id", currTaskId, "target_id", targetId, "content", m.Content, "at_user", m.AtUser)
|
||||
Info("📩 发送文本任务执行结果", "result", result, "task_id", currTaskId, "target_id", targetId, "at_user", m.AtUser)
|
||||
case "image":
|
||||
targetPath, md5Str, err := SaveBase64Image(m.Content)
|
||||
if err != nil {
|
||||
@@ -57,12 +66,137 @@ func SendWechatMsg(m *SendMsg) {
|
||||
case "send_image":
|
||||
result := fridaScript.ExportsCall("triggerSendImgMessage", currTaskId, myWechatId, targetId)
|
||||
Info("📩 发送图片任务执行结果", "result", result, "task_id", currTaskId, "wechat_id", myWechatId, "target_id", targetId)
|
||||
case "download":
|
||||
result := fridaScript.ExportsCall("triggerDownload", targetId, m.FIleCdnUrl, m.AesKey, m.FilePath, m.FileType)
|
||||
Info("📩 下载图片任务执行结果", "result", result, "task_id", currTaskId, "wechat_id", myWechatId, "target_id", targetId)
|
||||
}
|
||||
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
Info("任务执行超时!", "taskId", currTaskId)
|
||||
Error("任务执行超时!", "taskId", currTaskId)
|
||||
case <-finishChan:
|
||||
Info("收到完成信号,任务完成", "taskId", currTaskId)
|
||||
}
|
||||
}
|
||||
|
||||
func HandleMsg(jsonData []byte) ([]byte, error) {
|
||||
m := new(WechatMessage)
|
||||
err := json.Unmarshal(jsonData, m)
|
||||
if err != nil {
|
||||
Error("解析消息失败", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
myWechatId = m.SelfID
|
||||
if m.GroupId != "" {
|
||||
userID2NicknameMap.Store(m.GroupId+"_"+m.UserID, m.Sender.Nickname)
|
||||
}
|
||||
|
||||
for _, msg := range m.Message {
|
||||
switch msg.Type {
|
||||
case "record":
|
||||
path, err := SaveAudioFile(msg.Data.Media)
|
||||
if err != nil {
|
||||
Error("保存音频失败", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
msg.Data.URL = "file://" + path
|
||||
msg.Data.Media = nil
|
||||
case "image":
|
||||
var fileMsg FileMsg
|
||||
err = xml.Unmarshal([]byte(msg.Data.Text), &fileMsg)
|
||||
if err != nil {
|
||||
Error("XML解析失败", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
path, err := GetDownloadPath(fileMsg.Image.MidImgURL, fileMsg.Image.AesKey)
|
||||
if err != nil {
|
||||
Error("获取文件路径失败", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msg.Data.URL = "file://" + path
|
||||
|
||||
case "file":
|
||||
var fileMsg FileMsg
|
||||
err = xml.Unmarshal([]byte(msg.Data.Text), &fileMsg)
|
||||
if err != nil {
|
||||
Error("XML解析失败", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
path, err := GetDownloadPath(fileMsg.AppMsg.AppAttach.CdnAttachURL, fileMsg.AppMsg.AppAttach.AesKey)
|
||||
if err != nil {
|
||||
Error("获取文件路径失败", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msg.Data.URL = "file://" + path
|
||||
case "face":
|
||||
var fileMsg FileMsg
|
||||
err = xml.Unmarshal([]byte(msg.Data.Text), &fileMsg)
|
||||
if err != nil {
|
||||
Error("XML解析失败", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
data, err := DownloadFile(fileMsg.Emoji.ThumbUrl)
|
||||
if err != nil {
|
||||
Error("下载表情失败", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
path, err := DetectAndSaveImage(data)
|
||||
if err != nil {
|
||||
Error("保存表情失败", "err", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
msg.Data.URL = "file://" + path
|
||||
}
|
||||
}
|
||||
return json.Marshal(m)
|
||||
}
|
||||
|
||||
func GetDownloadPath(cdnUrl, aesKeyStr string) (string, error) {
|
||||
for i := 0; i < 5; i++ {
|
||||
if downloadMsgInter, ok := userID2FileMsgMap.Load(cdnUrl); ok {
|
||||
downloadReq := downloadMsgInter.(*DownloadRequest)
|
||||
if downloadReq.FilePath != "" {
|
||||
return downloadReq.FilePath, nil
|
||||
}
|
||||
|
||||
// 检查数据是否还在接收中
|
||||
timeSinceLastAppend := time.Now().UnixMilli() - downloadReq.LastAppendTime
|
||||
Info("文件等待下载", "url", cdnUrl, "times", i, "last_append_time", timeSinceLastAppend)
|
||||
|
||||
// 如果数据仍在接收中(1秒内有新数据),继续等待
|
||||
if timeSinceLastAppend < 1000 && i < 4 {
|
||||
time.Sleep(2 * time.Second)
|
||||
continue
|
||||
}
|
||||
|
||||
// 数据接收完成,尝试解密
|
||||
if len(downloadReq.Media) > 0 {
|
||||
aesKey, err := hex.DecodeString(aesKeyStr)
|
||||
if err != nil {
|
||||
Error("AES key 解码失败", "err", err)
|
||||
return "", err
|
||||
}
|
||||
filePath, err := GetFilePath(downloadReq.Media, aesKey)
|
||||
if err != nil {
|
||||
Error("获取文件路径失败", "err", err, "media_len", len(downloadReq.Media))
|
||||
userID2FileMsgMap.Delete(cdnUrl)
|
||||
return "", err
|
||||
}
|
||||
|
||||
downloadReq.FilePath = filePath
|
||||
downloadReq.Media = nil
|
||||
return filePath, nil
|
||||
}
|
||||
}
|
||||
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
return "", errors.New("文件下载超时或数据为空")
|
||||
}
|
||||
|
||||
@@ -14,5 +14,6 @@
|
||||
"uploadOnCompleteAddr": "0x4913A88",
|
||||
"uploadOnCompleteFuncAddr": "0x36BB9C0",
|
||||
"downloadImagAddr": "0x4A6975C",
|
||||
"hdPicDownloadAddr": "0x494652C"
|
||||
"startDownloadMedia": "0x494663C",
|
||||
"downloadFileAddr": "0x4A084EC"
|
||||
}
|
||||
Reference in New Issue
Block a user