1. 支持转账、通话、链接消息的显示

2. 支持名片、视频号、QQ音乐、小程序、定位等消息的显示
3. 支持直播、游戏消息、语音通话、视频通话等消息的显示
4. 解密前判断对数据库是否有读权限
5. system消息html格式转文本
This commit is contained in:
HAL 2025-01-13 23:42:02 +08:00
parent f1a32a3f9a
commit c13e680b3d
18 changed files with 835 additions and 563 deletions

View File

@ -39,6 +39,16 @@ wails build
- [x] 支持链接消息
- [x] 支持语音消息
- [x] 支持文件消息
- [x] 支持名片消息
- [x] 支持定位消息
- [x] 支持视频/语音通话消息
- [x] 支持QQ音乐消息
- [x] 支持第三方视频软件分享消息
- [x] 支持分享表情集消息
- [x] 支持小程序消息
- [x] 支持视频号/直播消息
- [x] 支持转账消息
- [x] 支持腾讯游戏分享消息
- [x] 支持原始表情显示
- [x] 支持按类型检索
- [x] 支持日期检索

2
app.go
View File

@ -23,7 +23,7 @@ const (
configDefaultUserKey = "userConfig.defaultUser"
configUsersKey = "userConfig.users"
configExportPathKey = "exportPath"
appVersion = "v1.0.6"
appVersion = "v1.1.0"
)
type FileLoader struct {

View File

@ -1,3 +1,15 @@
## v1.1.0
1. 支持转账、通话、链接消息的显示
2. 支持名片、视频号、QQ音乐、小程序、定位等消息的显示
3. 支持直播、游戏消息、语音通话、视频通话等消息的显示
4. 解密前判断对数据库是否有读权限
5. system消息html格式转文本
## v1.0.7
1. 修复出现空数据库时不显示,聊天显示不全的问题
2. 解决安卓平板扫码登陆的情况下解密失败的问题
3. 移除lame和silk代码改用mod的方式引入
## v1.0.6
1. 图片/视频查看重新实现,保持与微信一致
2. 增加软件信息页面

BIN
frontend/dist/assets/applet.ce6471b1.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 KiB

533
frontend/dist/assets/index.04e85ebe.js vendored Normal file

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

BIN
frontend/dist/assets/map.b91d2cda.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.0 KiB

View File

@ -4,8 +4,8 @@
<meta charset="UTF-8"/>
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
<title>wechatDataBackup</title>
<script type="module" crossorigin src="/assets/index.d5e97187.js"></script>
<link rel="stylesheet" href="/assets/index.a11c4643.css">
<script type="module" crossorigin src="/assets/index.04e85ebe.js"></script>
<link rel="stylesheet" href="/assets/index.3ddb0aa4.css">
</head>
<body >
<div id="root"></div>

2
go.mod
View File

@ -13,6 +13,7 @@ require (
github.com/shirou/gopsutil/v3 v3.24.2
github.com/spf13/viper v1.18.2
github.com/wailsapp/wails/v2 v2.9.1
golang.org/x/net v0.25.0
golang.org/x/sys v0.20.0
google.golang.org/protobuf v1.31.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
@ -62,7 +63,6 @@ require (
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/text v0.15.0 // indirect
gopkg.in/ini.v1 v1.67.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect

View File

@ -1,6 +1,8 @@
package utils
import (
"crypto/md5"
"encoding/hex"
"errors"
"fmt"
"io"
@ -8,10 +10,12 @@ import (
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
"github.com/pkg/browser"
"github.com/shirou/gopsutil/v3/disk"
"golang.org/x/net/html"
"golang.org/x/sys/windows/registry"
)
@ -150,3 +154,79 @@ func CopyFile(src, dst string) (int64, error) {
return bytesWritten, nil
}
func extractTextFromHTML(htmlStr string) string {
doc, err := html.Parse(strings.NewReader(htmlStr))
if err != nil {
fmt.Println("Error parsing HTML:", err)
return ""
}
var extractText func(*html.Node) string
extractText = func(n *html.Node) string {
if n.Type == html.TextNode {
return n.Data
}
var text string
for c := n.FirstChild; c != nil; c = c.NextSibling {
text += extractText(c)
}
return text
}
return extractText(doc)
}
func removeCustomTags(input string) string {
re := regexp.MustCompile(`<(_wc_custom_link_)[^>]*?>`)
return re.ReplaceAllString(input, `$2`)
}
func Html2Text(htmlStr string) string {
if htmlStr[0] != '<' {
return htmlStr
}
text := extractTextFromHTML(htmlStr)
if strings.Contains(text, `<_wc_custom_link_`) {
text = "\U0001F9E7" + removeCustomTags(text)
}
return text
}
func HtmlMsgGetAttr(htmlStr, tag string) map[string]string {
doc, err := html.Parse(strings.NewReader(htmlStr))
if err != nil {
fmt.Println("Error parsing HTML:", err)
return nil
}
var attributes map[string]string
var findAttributes func(*html.Node)
findAttributes = func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == tag {
attributes = make(map[string]string)
for _, attr := range n.Attr {
attributes[attr.Key] = attr.Val
}
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
findAttributes(c)
}
}
findAttributes(doc)
return attributes
}
func Hash256Sum(data []byte) string {
hash := md5.New()
hash.Write([]byte(data))
hashSum := hash.Sum(nil)
return hex.EncodeToString(hashSum)
}

View File

@ -697,6 +697,12 @@ func Is64BitProcess(pid uint32) (bool, error) {
}
func GetWeChatKey(info *WeChatInfo) string {
mediaDB := info.FilePath + "\\Msg\\Media.db"
if _, err := os.Stat(mediaDB); err != nil {
log.Printf("open db %s error: %v", mediaDB, err)
return ""
}
handle, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_VM_READ, false, uint32(info.ProcessID))
if err != nil {
log.Println("Error opening process:", err)

View File

@ -13,6 +13,7 @@ import (
"strings"
sync "sync"
"time"
"wechatDataBackup/pkg/utils"
"github.com/beevik/etree"
_ "github.com/mattn/go-sqlite3"
@ -21,27 +22,36 @@ import (
)
const (
Wechat_Message_Type_Text = 1
Wechat_Message_Type_Picture = 3
Wechat_Message_Type_Voice = 34
Wechat_Message_Type_Video = 43
Wechat_Message_Type_Emoji = 47
Wechat_Message_Type_Misc = 49
Wechat_Message_Type_System = 10000
Wechat_Message_Type_Text = 1
Wechat_Message_Type_Picture = 3
Wechat_Message_Type_Voice = 34
Wechat_Message_Type_Visit_Card = 42
Wechat_Message_Type_Video = 43
Wechat_Message_Type_Emoji = 47
Wechat_Message_Type_Location = 48
Wechat_Message_Type_Misc = 49
Wechat_Message_Type_Voip = 50
Wechat_Message_Type_System = 10000
)
const (
Wechat_Misc_Message_TEXT = 1
Wechat_Misc_Message_Music = 3
Wechat_Misc_Message_ThirdVideo = 4
Wechat_Misc_Message_CardLink = 5
Wechat_Misc_Message_File = 6
Wechat_Misc_Message_CustomEmoji = 8
Wechat_Misc_Message_ShareEmoji = 15
Wechat_Misc_Message_ForwardMessage = 19
Wechat_Misc_Message_Applet = 33
Wechat_Misc_Message_Applet2 = 36
Wechat_Misc_Message_Channels = 51
Wechat_Misc_Message_Refer = 57
Wechat_Misc_Message_Live = 63
Wechat_Misc_Message_Game = 68
Wechat_Misc_Message_Notice = 87
Wechat_Misc_Message_Live2 = 88
Wechat_Misc_Message_TingListen = 92
Wechat_Misc_Message_Transfer = 2000
Wechat_Misc_Message_RedPacket = 2003
)
@ -107,9 +117,43 @@ type ReferInfo struct {
Content string `json:"Content"`
}
type PayInfo struct {
Type int
Memo string
BeginTime string
Feedesc string
}
type VoipInfo struct {
Type int
Msg string
}
type ChannelsInfo struct {
ThumbPath string
ThumbCache string
NickName string
}
type MusicInfo struct {
ThumbPath string
Title string
Description string
DisPlayName string
DataUrl string
}
type LocationInfo struct {
Label string
PoiName string
X string
Y string
ThumbPath string
}
type WeChatMessage struct {
LocalId int `json:"LocalId"`
MsgSvrId int64 `json:"MsgSvrId"`
MsgSvrId string `json:"MsgSvrId"`
Type int `json:"type"`
SubType int `json:"SubType"`
IsSender int `json:"IsSender"`
@ -126,6 +170,12 @@ type WeChatMessage struct {
UserInfo WeChatUserInfo `json:"userInfo"`
LinkInfo LinkInfo `json:"LinkInfo"`
ReferInfo ReferInfo `json:"ReferInfo"`
PayInfo PayInfo `json:"PayInfo"`
VoipInfo VoipInfo `json:"VoipInfo"`
VisitInfo WeChatUserInfo `json:"VisitInfo"`
ChannelsInfo ChannelsInfo `json:"ChannelsInfo"`
MusicInfo MusicInfo `json:"MusicInfo"`
LocationInfo LocationInfo `json:"LocationInfo"`
compressContent []byte
bytesExtra []byte
}
@ -400,7 +450,7 @@ func (P *WechatDataProvider) WeChatGetSessionList(pageIndex int, pageSize int) (
List := &WeChatSessionList{}
List.Rows = make([]WeChatSession, 0)
querySql := fmt.Sprintf("select ifnull(strUsrName,'') as strUsrName,ifnull(strNickName,'') as strNickName,ifnull(strContent,'') as strContent, nTime from Session order by nOrder desc limit %d, %d;", pageIndex*pageSize, pageSize)
querySql := fmt.Sprintf("select ifnull(strUsrName,'') as strUsrName,ifnull(strNickName,'') as strNickName,ifnull(strContent,'') as strContent, nMsgType, nTime from Session order by nOrder desc limit %d, %d;", pageIndex*pageSize, pageSize)
dbRows, err := P.microMsg.Query(querySql)
if err != nil {
log.Println(err)
@ -410,9 +460,10 @@ func (P *WechatDataProvider) WeChatGetSessionList(pageIndex int, pageSize int) (
var strUsrName, strNickName, strContent string
var nTime uint64
var nMsgType int
for dbRows.Next() {
var session WeChatSession
err = dbRows.Scan(&strUsrName, &strNickName, &strContent, &nTime)
err = dbRows.Scan(&strUsrName, &strNickName, &strContent, &nMsgType, &nTime)
if err != nil {
log.Println(err)
continue
@ -424,7 +475,7 @@ func (P *WechatDataProvider) WeChatGetSessionList(pageIndex int, pageSize int) (
session.UserName = strUsrName
session.NickName = strNickName
session.Content = revokemsg_parse(strContent)
session.Content = systemMsgParse(nMsgType, strContent)
session.Time = nTime
session.IsGroup = strings.HasSuffix(strUsrName, "@chatroom")
info, err := P.WechatGetUserInfoByNameOnCache(strUsrName)
@ -555,14 +606,15 @@ func (P *WechatDataProvider) weChatGetMessageListByTime(userName string, time in
log.Println("rows.Scan failed", err)
return List, err
}
message.LocalId = localId
message.MsgSvrId = MsgSvrID
message.MsgSvrId = fmt.Sprintf("%d", MsgSvrID)
message.Type = Type
message.SubType = SubType
message.IsSender = IsSender
message.CreateTime = CreateTime
message.Talker = StrTalker
message.Content = revokemsg_parse(StrContent)
message.Content = systemMsgParse(Type, StrContent)
message.IsChatRoom = strings.HasSuffix(StrTalker, "@chatroom")
message.compressContent = make([]byte, len(CompressContent))
message.bytesExtra = make([]byte, len(BytesExtra))
@ -572,6 +624,9 @@ func (P *WechatDataProvider) weChatGetMessageListByTime(userName string, time in
P.wechatMessageGetUserInfo(&message)
P.wechatMessageEmojiHandle(&message)
P.wechatMessageCompressContentHandle(&message)
P.wechatMessageVoipHandle(&message)
P.wechatMessageVisitHandke(&message)
P.wechatMessageLocationHandke(&message)
List.Rows = append(List.Rows, message)
List.Total += 1
}
@ -805,8 +860,16 @@ func (P *WechatDataProvider) wechatMessageExtraHandle(msg *WeChatMessage) {
msg.UserInfo.UserName = ext.Field2
}
case 3:
if len(ext.Field2) > 0 && (msg.Type == Wechat_Message_Type_Picture || msg.Type == Wechat_Message_Type_Video || msg.Type == Wechat_Message_Type_Misc) {
msg.ThumbPath = P.prefixResPath + ext.Field2[len(P.SelfInfo.UserName):]
if len(ext.Field2) > 0 {
if msg.Type == Wechat_Message_Type_Picture || msg.Type == Wechat_Message_Type_Video || msg.Type == Wechat_Message_Type_Misc {
msg.ThumbPath = P.prefixResPath + ext.Field2[len(P.SelfInfo.UserName):]
}
if msg.Type == Wechat_Message_Type_Misc && (msg.SubType == Wechat_Misc_Message_Music || msg.SubType == Wechat_Misc_Message_TingListen) {
msg.MusicInfo.ThumbPath = P.prefixResPath + ext.Field2[len(P.SelfInfo.UserName):]
} else if msg.Type == Wechat_Message_Type_Location {
msg.LocationInfo.ThumbPath = P.prefixResPath + ext.Field2[len(P.SelfInfo.UserName):]
}
}
case 4:
if len(ext.Field2) > 0 {
@ -822,7 +885,7 @@ func (P *WechatDataProvider) wechatMessageExtraHandle(msg *WeChatMessage) {
}
if msg.Type == Wechat_Message_Type_Voice {
msg.VoicePath = fmt.Sprintf("%s\\FileStorage\\Voice\\%d.mp3", P.prefixResPath, msg.MsgSvrId)
msg.VoicePath = fmt.Sprintf("%s\\FileStorage\\Voice\\%s.mp3", P.prefixResPath, msg.MsgSvrId)
}
}
@ -889,9 +952,8 @@ func (P *WechatDataProvider) wechatMessageCompressContentHandle(msg *WeChatMessa
log.Println("ReadFromBytes failed:", err)
return
}
root := NewxmlDocument(compMsg)
if msg.Type == Wechat_Message_Type_Misc && msg.SubType == Wechat_Misc_Message_CardLink {
if msg.Type == Wechat_Message_Type_Misc && isLinkSubType(msg.SubType) {
msg.LinkInfo.Title = root.FindElementValue("/msg/appmsg/title")
msg.LinkInfo.Description = root.FindElementValue("/msg/appmsg/des")
msg.LinkInfo.Url = root.FindElementValue("/msg/appmsg/url")
@ -900,6 +962,10 @@ func (P *WechatDataProvider) wechatMessageCompressContentHandle(msg *WeChatMessa
if len(msg.LinkInfo.DisPlayName) == 0 && len(appName) > 0 {
msg.LinkInfo.DisPlayName = appName
}
thumburl := root.FindElementValue("/msg/appmsg/thumburl")
if len(msg.ThumbPath) == 0 && len(thumburl) > 0 && strings.HasPrefix(thumburl, "http") {
msg.ThumbPath = thumburl
}
} else if msg.Type == Wechat_Message_Type_Misc && msg.SubType == Wechat_Misc_Message_Refer {
msg.Content = root.FindElementValue("/msg/appmsg/title")
msg.ReferInfo.Type, _ = strconv.Atoi(root.FindElementValue("/msg/appmsg/refermsg/type"))
@ -918,9 +984,80 @@ func (P *WechatDataProvider) wechatMessageCompressContentHandle(msg *WeChatMessa
msg.ReferInfo.Content = root.FindElementValue("/msg/appmsg/title")
msg.ReferInfo.SubType, _ = strconv.Atoi(root.FindElementValue("/msg/appmsg/type"))
}
} else if msg.Type == Wechat_Message_Type_Misc && msg.SubType == Wechat_Misc_Message_Transfer {
msg.PayInfo.Type, _ = strconv.Atoi(root.FindElementValue("/msg/appmsg/wcpayinfo/paysubtype"))
msg.PayInfo.Feedesc = root.FindElementValue("/msg/appmsg/wcpayinfo/feedesc")
msg.PayInfo.BeginTime = root.FindElementValue("/msg/appmsg/wcpayinfo/begintransfertime")
msg.PayInfo.Memo = root.FindElementValue("/msg/appmsg/wcpayinfo/pay_memo")
} else if msg.Type == Wechat_Message_Type_Misc && msg.SubType == Wechat_Misc_Message_TEXT {
msg.Content = root.FindElementValue("/msg/appmsg/title")
} else if msg.Type == Wechat_Message_Type_Misc && msg.SubType == Wechat_Misc_Message_Channels {
msg.ChannelsInfo.NickName = root.FindElementValue("/msg/appmsg/finderFeed/nickname")
msg.ChannelsInfo.ThumbPath = root.FindElementValue("/msg/appmsg/finderFeed/mediaList/media/thumbUrl")
msg.ChannelsInfo.ThumbPath = P.urlconvertCacheName(msg.ChannelsInfo.ThumbPath, msg.CreateTime)
} else if msg.Type == Wechat_Message_Type_Misc && msg.SubType == Wechat_Misc_Message_Live {
msg.ChannelsInfo.NickName = root.FindElementValue("/msg/appmsg/finderLive/nickname")
msg.ChannelsInfo.ThumbPath = root.FindElementValue("/msg/appmsg/finderLive/media/coverUrl")
msg.ChannelsInfo.ThumbPath = P.urlconvertCacheName(msg.ChannelsInfo.ThumbPath, msg.CreateTime)
} else if msg.Type == Wechat_Message_Type_Misc && (msg.SubType == Wechat_Misc_Message_Music || msg.SubType == Wechat_Misc_Message_TingListen) {
msg.MusicInfo.Title = root.FindElementValue("/msg/appmsg/title")
msg.MusicInfo.Description = root.FindElementValue("/msg/appmsg/des")
msg.MusicInfo.DataUrl = root.FindElementValue("/msg/appmsg/dataurl")
msg.MusicInfo.DisPlayName = root.FindElementValue("/msg/appinfo/appname")
}
}
func (P *WechatDataProvider) wechatMessageVoipHandle(msg *WeChatMessage) {
if msg.Type != Wechat_Message_Type_Voip {
return
}
xmlMsg := etree.NewDocument()
if err := xmlMsg.ReadFromBytes([]byte(msg.Content)); err != nil {
// os.WriteFile("D:\\tmp\\"+string(msg.LocalId)+".xml", unCompressContent[:ulen], 0600)
log.Println("ReadFromBytes failed:", err)
return
}
root := NewxmlDocument(xmlMsg)
msg.VoipInfo.Type, _ = strconv.Atoi(root.FindElementValue("/voipmsg/VoIPBubbleMsg/room_type"))
msg.VoipInfo.Msg = root.FindElementValue("/voipmsg/VoIPBubbleMsg/msg")
}
func (P *WechatDataProvider) wechatMessageVisitHandke(msg *WeChatMessage) {
if msg.Type != Wechat_Message_Type_Visit_Card {
return
}
attr := utils.HtmlMsgGetAttr(msg.Content, "msg")
userName, exists := attr["username"]
if !exists {
return
}
userInfo, err := P.WechatGetUserInfoByNameOnCache(userName)
if err == nil {
msg.VisitInfo = *userInfo
} else {
msg.VisitInfo.UserName = userName
msg.VisitInfo.Alias = attr["alias"]
msg.VisitInfo.NickName = attr["nickname"]
msg.VisitInfo.SmallHeadImgUrl = attr["smallheadimgurl"]
msg.VisitInfo.BigHeadImgUrl = attr["bigheadimgurl"]
}
}
func (P *WechatDataProvider) wechatMessageLocationHandke(msg *WeChatMessage) {
if msg.Type != Wechat_Message_Type_Location {
return
}
attr := utils.HtmlMsgGetAttr(msg.Content, "location")
msg.LocationInfo.Label = attr["label"]
msg.LocationInfo.PoiName = attr["poiname"]
msg.LocationInfo.X = attr["x"]
msg.LocationInfo.Y = attr["y"]
}
func (P *WechatDataProvider) wechatMessageGetUserInfo(msg *WeChatMessage) {
who := msg.Talker
if msg.IsSender == 1 {
@ -1031,9 +1168,11 @@ func weChatMessageContains(msg *WeChatMessage, chars string) bool {
switch msg.Type {
case Wechat_Message_Type_Text:
return strings.Contains(msg.Content, chars)
case Wechat_Message_Type_Location:
return strings.Contains(msg.LocationInfo.Label, chars) || strings.Contains(msg.LocationInfo.PoiName, chars)
case Wechat_Message_Type_Misc:
switch msg.SubType {
case Wechat_Misc_Message_CardLink:
case Wechat_Misc_Message_CardLink, Wechat_Misc_Message_ThirdVideo, Wechat_Misc_Message_Applet, Wechat_Misc_Message_Applet2:
return strings.Contains(msg.LinkInfo.Title, chars) || strings.Contains(msg.LinkInfo.Description, chars)
case Wechat_Misc_Message_Refer:
return strings.Contains(msg.Content, chars)
@ -1056,7 +1195,7 @@ func weChatMessageTypeFilter(msg *WeChatMessage, msgType string) bool {
case "图片与视频":
return msg.Type == Wechat_Message_Type_Picture || msg.Type == Wechat_Message_Type_Video
case "链接":
return msg.Type == Wechat_Misc_Message_CardLink || msg.SubType == Wechat_Misc_Message_CardLink
return msg.Type == Wechat_Message_Type_Misc && (msg.SubType == Wechat_Misc_Message_CardLink || msg.SubType == Wechat_Misc_Message_ThirdVideo)
default:
if strings.HasPrefix(msgType, "群成员") {
userName := msgType[len("群成员"):]
@ -1219,11 +1358,36 @@ func WechatGetAccountInfo(resPath, prefixRes, accountName string) (*WeChatAccoun
return info, nil
}
func revokemsg_parse(content string) string {
if strings.HasPrefix(content, "<revokemsg>") && strings.HasSuffix(content, "</revokemsg>") {
trimmed := strings.TrimSuffix(strings.TrimPrefix(content, "<revokemsg>"), "</revokemsg>")
return trimmed
func systemMsgParse(msgType int, content string) string {
if msgType != Wechat_Message_Type_System {
return content
}
return content
return utils.Html2Text(content)
}
func (P *WechatDataProvider) urlconvertCacheName(url string, timestamp int64) string {
t := time.Unix(timestamp, 0)
yearMonth := t.Format("2006-01")
md5String := utils.Hash256Sum([]byte(url))
realPath := fmt.Sprintf("%s\\FileStorage\\Cache\\%s\\%s.jpg", P.resPath, yearMonth, md5String)
path := fmt.Sprintf("%s\\FileStorage\\Cache\\%s\\%s.jpg", P.prefixResPath, yearMonth, md5String)
if _, err := os.Stat(realPath); err == nil {
return path
}
return url
}
func isLinkSubType(subType int) bool {
targetSubTypes := map[int]bool{
Wechat_Misc_Message_CardLink: true,
Wechat_Misc_Message_ThirdVideo: true,
Wechat_Misc_Message_ShareEmoji: true,
Wechat_Misc_Message_Applet: true,
Wechat_Misc_Message_Applet2: true,
Wechat_Misc_Message_Game: true,
}
return targetSubTypes[subType]
}