1. 图片/视频查看重新实现,保持与微信一致

2. 增加软件信息页面
3. 修复企业联系人头像和名称不显示问题
4. 修复撤回消息显示不正常的问题
This commit is contained in:
HAL 2024-12-14 14:56:45 +08:00
parent 30ccdd6c65
commit d34225785c
14 changed files with 874 additions and 1031 deletions

View File

@ -9,6 +9,7 @@ PC微信聊天记录数据导出工具
效果图如下:
![](./res/result.png)
![](./res/result2.png)
## 演示视频
[演示视频](https://www.bilibili.com/video/BV1bPH1eWEEy/?share_source=copy_web&vd_source=b5cfa9258a9ad9900a00e9c1ce3cb4b6)
@ -47,7 +48,7 @@ wails build
- [x] 多开账号数据切换
- [x] 头像使用本地头像
- [ ] 支持更多消息类型显示
- [ ] 图片查看器重绘
- [x] 图片查看器重绘
- [ ] 实现表情预先下载(实现完全离线查看)
- [ ] 聊天报告
- [ ] AI本地模型应用
@ -64,6 +65,10 @@ A: 这是由于可能数据存在于内存中还没有回写到磁盘导致的
**Q: 有些图片、视频打不开**<br>
A: 这是电脑端微信没有点开过这个消息,默认只加载了预览图而已,如果手机有打开过可以把手机的记录迁移到电脑,迁移后重新退出登陆一次微信导出即可。<br>
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=git-jiadong/wechatDataBackup&type=Date)](https://star-history.com/?utm_source=bestxtools.com#git-jiadong/wechatDataBackup&Date)
## 免责声明
**⚠️ 本项目仅供学习、研究使用,严禁商业使用**<br/>
**⚠️ 用于网络安全用途的,请确保在国家法律法规下使用**<br/>

141
app.go
View File

@ -5,9 +5,11 @@ import (
"encoding/json"
"fmt"
"log"
"mime"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"wechatDataBackup/pkg/utils"
"wechatDataBackup/pkg/wechat"
@ -21,7 +23,7 @@ const (
configDefaultUserKey = "userConfig.defaultUser"
configUsersKey = "userConfig.users"
configExportPathKey = "exportPath"
appVersion = "v1.0.5"
appVersion = "v1.0.6"
)
type FileLoader struct {
@ -30,6 +32,7 @@ type FileLoader struct {
}
func NewFileLoader(prefix string) *FileLoader {
mime.AddExtensionType(".mp3", "audio/mpeg")
return &FileLoader{FilePrefix: prefix}
}
@ -39,16 +42,74 @@ func (h *FileLoader) SetFilePrefix(prefix string) {
}
func (h *FileLoader) ServeHTTP(res http.ResponseWriter, req *http.Request) {
var err error
requestedFilename := h.FilePrefix + "\\" + strings.TrimPrefix(req.URL.Path, "/")
// log.Println("Requesting file:", requestedFilename)
fileData, err := os.ReadFile(requestedFilename)
file, err := os.Open(requestedFilename)
if err != nil {
res.WriteHeader(http.StatusBadRequest)
res.Write([]byte(fmt.Sprintf("Could not load file %s", requestedFilename)))
http.Error(res, fmt.Sprintf("Could not load file %s", requestedFilename), http.StatusBadRequest)
return
}
defer file.Close()
fileInfo, err := file.Stat()
if err != nil {
http.Error(res, "Could not retrieve file info", http.StatusInternalServerError)
return
}
res.Write(fileData)
fileSize := fileInfo.Size()
rangeHeader := req.Header.Get("Range")
if rangeHeader == "" {
// 无 Range 请求,直接返回整个文件
res.Header().Set("Content-Length", strconv.FormatInt(fileSize, 10))
http.ServeContent(res, req, requestedFilename, fileInfo.ModTime(), file)
return
}
var start, end int64
if strings.HasPrefix(rangeHeader, "bytes=") {
ranges := strings.Split(strings.TrimPrefix(rangeHeader, "bytes="), "-")
start, _ = strconv.ParseInt(ranges[0], 10, 64)
if len(ranges) > 1 && ranges[1] != "" {
end, _ = strconv.ParseInt(ranges[1], 10, 64)
} else {
end = fileSize - 1
}
} else {
http.Error(res, "Invalid Range header", http.StatusRequestedRangeNotSatisfiable)
return
}
if start < 0 || end >= fileSize || start > end {
http.Error(res, "Requested range not satisfiable", http.StatusRequestedRangeNotSatisfiable)
return
}
contentType := mime.TypeByExtension(filepath.Ext(requestedFilename))
if contentType == "" {
contentType = "application/octet-stream"
}
res.Header().Set("Content-Type", contentType)
res.Header().Set("Content-Range", fmt.Sprintf("bytes %d-%d/%d", start, end, fileSize))
res.Header().Set("Content-Length", strconv.FormatInt(end-start+1, 10))
res.WriteHeader(http.StatusPartialContent)
buffer := make([]byte, 102400)
file.Seek(start, 0)
for current := start; current <= end; {
readSize := int64(len(buffer))
if end-current+1 < readSize {
readSize = end - current + 1
}
n, err := file.Read(buffer[:readSize])
if err != nil {
break
}
res.Write(buffer[:n])
current += int64(n)
}
}
// App struct
@ -90,7 +151,7 @@ type ErrorMessage struct {
// NewApp creates a new App application struct
func NewApp() *App {
a := &App{}
log.Println("App version:", appVersion)
a.firstInit = true
a.FLoader = NewFileLoader(".\\")
viper.SetConfigName(defaultConfig)
@ -306,7 +367,7 @@ func (a *App) GetWechatContactList(pageIndex int, pageSize int) string {
}
func (a *App) GetWechatMessageListByTime(userName string, time int64, pageSize int, direction string) string {
log.Println("GetWechatMessageList:", userName, pageSize, time, direction)
log.Println("GetWechatMessageListByTime:", userName, pageSize, time, direction)
if len(userName) == 0 {
return "{\"Total\":0, \"Rows\":[]}"
}
@ -318,11 +379,33 @@ func (a *App) GetWechatMessageListByTime(userName string, time int64, pageSize i
}
list, err := a.provider.WeChatGetMessageListByTime(userName, time, pageSize, dire)
if err != nil {
log.Println("WeChatGetMessageList failed:", err)
log.Println("GetWechatMessageListByTime failed:", err)
return ""
}
listStr, _ := json.Marshal(list)
log.Println("GetWechatMessageList:", list.Total)
log.Println("GetWechatMessageListByTime:", list.Total)
return string(listStr)
}
func (a *App) GetWechatMessageListByType(userName string, time int64, pageSize int, msgType string, direction string) string {
log.Println("GetWechatMessageListByType:", userName, pageSize, time, msgType, direction)
if len(userName) == 0 {
return "{\"Total\":0, \"Rows\":[]}"
}
dire := wechat.Message_Search_Forward
if direction == "backward" {
dire = wechat.Message_Search_Backward
} else if direction == "both" {
dire = wechat.Message_Search_Both
}
list, err := a.provider.WeChatGetMessageListByType(userName, time, pageSize, msgType, dire)
if err != nil {
log.Println("WeChatGetMessageListByType failed:", err)
return ""
}
listStr, _ := json.Marshal(list)
log.Println("WeChatGetMessageListByType:", list.Total)
return string(listStr)
}
@ -594,3 +677,39 @@ func (a *App) scanAccountByPath(path string) error {
func (a *App) OepnLogFileExplorer() {
utils.OpenFileOrExplorer(".\\app.log", true)
}
func (a *App) SaveFileDialog(file string, alisa string) string {
filePath := a.FLoader.FilePrefix + file
if _, err := os.Stat(filePath); err != nil {
log.Println("SaveFileDialog:", err)
return err.Error()
}
savePath, err := runtime.SaveFileDialog(a.ctx, runtime.SaveDialogOptions{
DefaultFilename: alisa,
Title: "选择保存路径",
})
if err != nil {
log.Println("SaveFileDialog:", err)
return err.Error()
}
if savePath == "" {
return ""
}
dirPath := filepath.Dir(savePath)
if !utils.PathIsCanWriteFile(dirPath) {
errStr := "Path Is Can't Write File: " + filepath.Dir(savePath)
log.Println(errStr)
return errStr
}
_, err = utils.CopyFile(filePath, savePath)
if err != nil {
log.Println("Error CopyFile", filePath, savePath, err)
return err.Error()
}
return ""
}

View File

@ -1,3 +1,9 @@
## v1.0.6
1. 图片/视频查看重新实现,保持与微信一致
2. 增加软件信息页面
3. 修复企业联系人头像和名称不显示问题
4. 修复撤回消息显示不正常的问题
## v1.0.5
1. 修复联系人搜索显示异常的问题
2. 修复启动时界面加载显示慢的问题

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

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

533
frontend/dist/assets/index.d5e97187.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

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.5130ac24.js"></script>
<link rel="stylesheet" href="/assets/index.f70722f4.css">
<script type="module" crossorigin src="/assets/index.d5e97187.js"></script>
<link rel="stylesheet" href="/assets/index.a11c4643.css">
</head>
<body >
<div id="root"></div>

View File

@ -1,7 +1,9 @@
package utils
import (
"errors"
"fmt"
"io"
"log"
"os"
"os/exec"
@ -120,3 +122,31 @@ func PathIsCanWriteFile(path string) bool {
return true
}
func CopyFile(src, dst string) (int64, error) {
stat, err := os.Stat(src)
if err != nil {
return 0, err
}
if stat.IsDir() {
return 0, errors.New(src + " is dir")
}
sourceFile, err := os.Open(src)
if err != nil {
return 0, err
}
defer sourceFile.Close()
destFile, err := os.Create(dst)
if err != nil {
return 0, err
}
defer destFile.Close()
bytesWritten, err := io.Copy(destFile, sourceFile)
if err != nil {
return bytesWritten, err
}
return bytesWritten, nil
}

View File

@ -180,17 +180,18 @@ type WechatDataProvider struct {
resPath string
prefixResPath string
microMsg *sql.DB
msgDBs []*wechatMsgDB
userInfoMap map[string]WeChatUserInfo
userInfoMtx sync.Mutex
openIMContact *sql.DB
msgDBs []*wechatMsgDB
userInfoMap map[string]WeChatUserInfo
userInfoMtx sync.Mutex
SelfInfo *WeChatUserInfo
ContactList *WeChatContactList
}
const (
MicroMsgDB = "MicroMsg.db"
MicroMsgDB = "MicroMsg.db"
OpenIMContactDB = "OpenIMContact.db"
)
type byTime []*wechatMsgDB
@ -239,6 +240,15 @@ func CreateWechatDataProvider(resPath string, prefixRes string) (*WechatDataProv
return provider, err
}
var openIMContact *sql.DB
OpenIMContactDBPath := resPath + "\\Msg\\" + OpenIMContactDB
if _, err := os.Stat(OpenIMContactDBPath); err == nil {
openIMContact, err = sql.Open("sqlite3", OpenIMContactDBPath)
if err != nil {
log.Printf("open db %s error: %v", OpenIMContactDBPath, err)
}
}
index := 0
for {
msgDBPath := fmt.Sprintf("%s\\Msg\\Multi\\MSG%d.db", provider.resPath, index)
@ -262,6 +272,7 @@ func CreateWechatDataProvider(resPath string, prefixRes string) (*WechatDataProv
}
provider.userInfoMap = make(map[string]WeChatUserInfo)
provider.microMsg = microMsg
provider.openIMContact = openIMContact
provider.SelfInfo, err = provider.WechatGetUserInfoByNameOnCache(userName)
if err != nil {
log.Printf("WechatGetUserInfoByName %s failed: %v", userName, err)
@ -288,6 +299,13 @@ func (P *WechatDataProvider) WechatWechatDataProviderClose() {
}
}
if P.openIMContact != nil {
err := P.openIMContact.Close()
if err != nil {
log.Println("db close:", err)
}
}
for _, db := range P.msgDBs {
err := db.db.Close()
if err != nil {
@ -336,6 +354,47 @@ func (P *WechatDataProvider) WechatGetUserInfoByName(name string) (*WeChatUserIn
return info, nil
}
func (P *WechatDataProvider) WechatGetOpenIMMUserInfoByName(name string) (*WeChatUserInfo, error) {
info := &WeChatUserInfo{}
var UserName, ReMark, NickName string
querySql := fmt.Sprintf("select ifnull(UserName,'') as UserName, ifnull(ReMark,'') as ReMark, ifnull(NickName,'') as NickName from OpenIMContact where UserName='%s';", name)
// log.Println(querySql)
if P.openIMContact != nil {
err := P.openIMContact.QueryRow(querySql).Scan(&UserName, &ReMark, &NickName)
if err != nil {
log.Println("not found User:", err)
return info, err
}
}
log.Printf("UserName %s, ReMark %s, NickName %s\n", UserName, ReMark, NickName)
var smallHeadImgUrl, bigHeadImgUrl string
querySql = fmt.Sprintf("select ifnull(smallHeadImgUrl,'') as smallHeadImgUrl, ifnull(bigHeadImgUrl,'') as bigHeadImgUrl from ContactHeadImgUrl where usrName='%s';", UserName)
// log.Println(querySql)
err := P.microMsg.QueryRow(querySql).Scan(&smallHeadImgUrl, &bigHeadImgUrl)
if err != nil {
log.Println("not find headimg", err)
}
info.UserName = UserName
info.Alias = ""
info.ReMark = ReMark
info.NickName = NickName
info.SmallHeadImgUrl = smallHeadImgUrl
info.BigHeadImgUrl = bigHeadImgUrl
info.IsGroup = strings.HasSuffix(UserName, "@chatroom")
localHeadImgPath := fmt.Sprintf("%s\\FileStorage\\HeadImage\\%s.headimg", P.resPath, name)
relativePath := fmt.Sprintf("%s\\FileStorage\\HeadImage\\%s.headimg", P.prefixResPath, name)
if _, err = os.Stat(localHeadImgPath); err == nil {
info.LocalHeadImgUrl = relativePath
}
// log.Println(info)
return info, nil
}
func (P *WechatDataProvider) WeChatGetSessionList(pageIndex int, pageSize int) (*WeChatSessionList, error) {
List := &WeChatSessionList{}
List.Rows = make([]WeChatSession, 0)
@ -364,7 +423,7 @@ func (P *WechatDataProvider) WeChatGetSessionList(pageIndex int, pageSize int) (
session.UserName = strUsrName
session.NickName = strNickName
session.Content = strContent
session.Content = revokemsg_parse(strContent)
session.Time = nTime
session.IsGroup = strings.HasSuffix(strUsrName, "@chatroom")
info, err := P.WechatGetUserInfoByNameOnCache(strUsrName)
@ -502,7 +561,7 @@ func (P *WechatDataProvider) weChatGetMessageListByTime(userName string, time in
message.IsSender = IsSender
message.CreateTime = CreateTime
message.Talker = StrTalker
message.Content = StrContent
message.Content = revokemsg_parse(StrContent)
message.IsChatRoom = strings.HasSuffix(StrTalker, "@chatroom")
message.compressContent = make([]byte, len(CompressContent))
message.bytesExtra = make([]byte, len(BytesExtra))
@ -564,6 +623,87 @@ func (P *WechatDataProvider) WeChatGetMessageListByKeyWord(userName string, time
return List, nil
}
func (P *WechatDataProvider) WeChatGetMessageListByType(userName string, time int64, pageSize int, msgType string, direction Message_Search_Direction) (*WeChatMessageList, error) {
List := &WeChatMessageList{}
List.Rows = make([]WeChatMessage, 0)
selectTime := time
selectpageSize := 30
needSize := pageSize
if msgType != "" {
selectpageSize = 600
}
if direction == Message_Search_Both {
needSize = pageSize / 2
}
for direction == Message_Search_Forward || direction == Message_Search_Both {
selectList, err := P.weChatGetMessageListByTime(userName, selectTime, selectpageSize, Message_Search_Forward)
if err != nil {
return List, err
}
if selectList.Total == 0 {
break
}
for i, _ := range selectList.Rows {
if weChatMessageTypeFilter(&selectList.Rows[i], msgType) {
List.Rows = append(List.Rows, selectList.Rows[i])
List.Total += 1
needSize -= 1
if needSize <= 0 {
break
}
}
}
if needSize <= 0 {
break
}
selectTime = selectList.Rows[selectList.Total-1].CreateTime - 1
log.Printf("Forward selectTime %d, selectpageSize %d needSize %d\n", selectTime, selectpageSize, needSize)
}
selectTime = time
if direction == Message_Search_Both {
needSize = pageSize / 2
}
for direction == Message_Search_Backward || direction == Message_Search_Both {
selectList, err := P.weChatGetMessageListByTime(userName, selectTime, selectpageSize, Message_Search_Backward)
if err != nil {
return List, err
}
if selectList.Total == 0 {
break
}
tmpTotal := 0
tmpRows := make([]WeChatMessage, 0)
for i := selectList.Total - 1; i >= 0; i-- {
if weChatMessageTypeFilter(&selectList.Rows[i], msgType) {
tmpRows = append([]WeChatMessage{selectList.Rows[i]}, tmpRows...)
tmpTotal += 1
needSize -= 1
if needSize <= 0 {
break
}
}
}
if tmpTotal > 0 {
List.Rows = append(tmpRows, List.Rows...)
List.Total += tmpTotal
}
selectTime = selectList.Rows[0].CreateTime + 1
if needSize <= 0 {
break
}
log.Printf("Backward selectTime %d, selectpageSize %d needSize %d\n", selectTime, selectpageSize, needSize)
}
return List, nil
}
func (P *WechatDataProvider) WeChatGetMessageDate(userName string) (*WeChatMessageDate, error) {
messageData := &WeChatMessageDate{}
messageData.Date = make([]string, 0)
@ -661,7 +801,7 @@ func (P *WechatDataProvider) wechatMessageExtraHandle(msg *WeChatMessage) {
switch ext.Field1 {
case 1:
if msg.IsChatRoom {
msg.Talker = ext.Field2
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) {
@ -784,6 +924,8 @@ func (P *WechatDataProvider) wechatMessageGetUserInfo(msg *WeChatMessage) {
who := msg.Talker
if msg.IsSender == 1 {
who = P.SelfInfo.UserName
} else if msg.IsChatRoom {
who = msg.UserInfo.UserName
}
pinfo, err := P.WechatGetUserInfoByNameOnCache(who)
@ -963,7 +1105,13 @@ func (P *WechatDataProvider) WechatGetUserInfoByNameOnCache(name string) (*WeCha
return &info, nil
}
pinfo, err := P.WechatGetUserInfoByName(name)
var pinfo *WeChatUserInfo
var err error
if strings.HasSuffix(name, "@openim") {
pinfo, err = P.WechatGetOpenIMMUserInfoByName(name)
} else {
pinfo, err = P.WechatGetUserInfoByName(name)
}
if err != nil {
log.Printf("WechatGetUserInfoByName %s failed: %v\n", name, err)
return nil, err
@ -1067,3 +1215,12 @@ func WechatGetAccountInfo(resPath, prefixRes, accountName string) (*WeChatAccoun
// log.Println(info)
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
}
return content
}

BIN
res/result2.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 772 KiB