Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
260c7306ad | ||
![]() |
26399847ee | ||
![]() |
640f27ce67 | ||
![]() |
6ddb45770f | ||
![]() |
4d9043cccf | ||
![]() |
3cc400bcbd | ||
![]() |
3d6d890212 | ||
![]() |
a8addf11eb | ||
![]() |
81b873048b | ||
![]() |
c13e680b3d |
5
.github/workflows/publish.yml
vendored
@ -29,11 +29,12 @@ jobs:
|
|||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- name: Build wails
|
- name: Build wails
|
||||||
uses: dAppServer/wails-build-action@v2.2
|
uses: dAppServer/wails-build-action@main
|
||||||
id: build
|
id: build
|
||||||
with:
|
with:
|
||||||
build-name: ${{ matrix.build.name }}
|
build-name: ${{ matrix.build.name }}
|
||||||
sign: false
|
sign: false
|
||||||
build-platform: ${{ matrix.build.platform }}
|
build-platform: ${{ matrix.build.platform }}
|
||||||
package: true
|
package: true
|
||||||
go-version: '1.21'
|
go-version: '1.21'
|
||||||
|
wails-version: "v2.9.1"
|
3
.gitignore
vendored
@ -30,4 +30,5 @@ env
|
|||||||
User
|
User
|
||||||
config.json
|
config.json
|
||||||
wechatDataBackup.exe
|
wechatDataBackup.exe
|
||||||
app.log
|
app.log
|
||||||
|
app-*.log
|
50
README.md
@ -1,5 +1,34 @@
|
|||||||
|
<p align="center" style="text-align: center">
|
||||||
|
<img src="./res/logo_256.png" width="15%"><br/>
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<b>wechatDataBackup: PC微信聊天记录数据导出工具</b>
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
<a href="https://github.com/git-jiadong/wechatDataBackup/stargazers">
|
||||||
|
<img src="https://img.shields.io/github/stars/git-jiadong/wechatDataBackup" alt="GitHub Star"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/git-jiadong/wechatDataBackup/releases">
|
||||||
|
<img src="https://img.shields.io/github/downloads/git-jiadong/wechatDataBackup/total" alt="downloads" />
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/git-jiadong/wechatDataBackup/releases">
|
||||||
|
<img src="https://img.shields.io/github/v/release/git-jiadong/wechatDataBackup" alt="releases version"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/git-jiadong/wechatDataBackup/commits/main">
|
||||||
|
<img src="https://img.shields.io/github/last-commit/git-jiadong/wechatDataBackup" alt="last commit" />
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/git-jiadong/wechatDataBackup" >
|
||||||
|
<img src="https://img.shields.io/github/languages/top/git-jiadong/wechatDataBackup" alt="languages"/>
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/git-jiadong/wechatDataBackup" >
|
||||||
|
<img src="https://img.shields.io/github/repo-size/git-jiadong/wechatDataBackup" alt="repo size" />
|
||||||
|
</a>
|
||||||
|
<a href="https://github.com/git-jiadong/wechatDataBackup/blob/main/LICENSE">
|
||||||
|
<img src="https://img.shields.io/github/license/git-jiadong/wechatDataBackup" alt="license" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
# wechatDataBackup
|
# wechatDataBackup
|
||||||
PC微信聊天记录数据导出工具
|
|
||||||
|
|
||||||
* 基于wails开发 + React前端,实现PC端微信聊天记录一键导出功能。
|
* 基于wails开发 + React前端,实现PC端微信聊天记录一键导出功能。
|
||||||
* 导出后数据可以做永久化保存,即使微信停止支持,聊天记录也可以随时查看。
|
* 导出后数据可以做永久化保存,即使微信停止支持,聊天记录也可以随时查看。
|
||||||
@ -39,6 +68,16 @@ wails build
|
|||||||
- [x] 支持链接消息
|
- [x] 支持链接消息
|
||||||
- [x] 支持语音消息
|
- [x] 支持语音消息
|
||||||
- [x] 支持文件消息
|
- [x] 支持文件消息
|
||||||
|
- [x] 支持名片消息
|
||||||
|
- [x] 支持定位消息
|
||||||
|
- [x] 支持视频/语音通话消息
|
||||||
|
- [x] 支持QQ音乐消息
|
||||||
|
- [x] 支持第三方视频软件分享消息
|
||||||
|
- [x] 支持分享表情集消息
|
||||||
|
- [x] 支持小程序消息
|
||||||
|
- [x] 支持视频号/直播消息
|
||||||
|
- [x] 支持转账消息
|
||||||
|
- [x] 支持腾讯游戏分享消息
|
||||||
- [x] 支持原始表情显示
|
- [x] 支持原始表情显示
|
||||||
- [x] 支持按类型检索
|
- [x] 支持按类型检索
|
||||||
- [x] 支持日期检索
|
- [x] 支持日期检索
|
||||||
@ -49,6 +88,10 @@ wails build
|
|||||||
- [x] 头像使用本地头像
|
- [x] 头像使用本地头像
|
||||||
- [ ] 支持更多消息类型显示
|
- [ ] 支持更多消息类型显示
|
||||||
- [x] 图片查看器重绘
|
- [x] 图片查看器重绘
|
||||||
|
- [x] 支持会话导出分享
|
||||||
|
- [x] 支持自动定位到最后浏览位置
|
||||||
|
- [x] 支持书签功能
|
||||||
|
- [x] 支持单聊会话对话人位置调换功能
|
||||||
- [ ] 实现表情预先下载(实现完全离线查看)
|
- [ ] 实现表情预先下载(实现完全离线查看)
|
||||||
- [ ] 聊天报告
|
- [ ] 聊天报告
|
||||||
- [ ] AI本地模型应用
|
- [ ] AI本地模型应用
|
||||||
@ -84,4 +127,7 @@ A: Win7电脑需要安装WebView2运行时才能正常使用。github release版
|
|||||||
- 微信数据库解密和数据库的使用 [PyWxDump](https://github.com/xaoyaoo/PyWxDump/tree/master)
|
- 微信数据库解密和数据库的使用 [PyWxDump](https://github.com/xaoyaoo/PyWxDump/tree/master)
|
||||||
- silk语音消息解码 [silk-v3-decoder](https://github.com/kn007/silk-v3-decoder)
|
- silk语音消息解码 [silk-v3-decoder](https://github.com/kn007/silk-v3-decoder)
|
||||||
- PCM转MP3 [lame](https://github.com/viert/lame.git)
|
- PCM转MP3 [lame](https://github.com/viert/lame.git)
|
||||||
- Dat图片解码 [wechatDatDecode](https://github.com/liuggchen/wechatDatDecode)
|
- Dat图片解码 [wechatDatDecode](https://github.com/liuggchen/wechatDatDecode)
|
||||||
|
|
||||||
|
## 交流/讨论
|
||||||
|

|
172
app.go
@ -23,7 +23,7 @@ const (
|
|||||||
configDefaultUserKey = "userConfig.defaultUser"
|
configDefaultUserKey = "userConfig.defaultUser"
|
||||||
configUsersKey = "userConfig.users"
|
configUsersKey = "userConfig.users"
|
||||||
configExportPathKey = "exportPath"
|
configExportPathKey = "exportPath"
|
||||||
appVersion = "v1.0.6"
|
appVersion = "v1.2.4"
|
||||||
)
|
)
|
||||||
|
|
||||||
type FileLoader struct {
|
type FileLoader struct {
|
||||||
@ -183,14 +183,15 @@ func (a *App) startup(ctx context.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
|
func (a *App) beforeClose(ctx context.Context) (prevent bool) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) shutdown(ctx context.Context) {
|
||||||
if a.provider != nil {
|
if a.provider != nil {
|
||||||
a.provider.WechatWechatDataProviderClose()
|
a.provider.WechatWechatDataProviderClose()
|
||||||
a.provider = nil
|
a.provider = nil
|
||||||
}
|
}
|
||||||
|
log.Printf("App Version %s exit!", appVersion)
|
||||||
return false
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) GetWeChatAllInfo() string {
|
func (a *App) GetWeChatAllInfo() string {
|
||||||
@ -713,3 +714,166 @@ func (a *App) SaveFileDialog(file string, alisa string) string {
|
|||||||
|
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) GetSessionLastTime(userName string) string {
|
||||||
|
if a.provider == nil || userName == "" {
|
||||||
|
lastTime := &wechat.WeChatLastTime{}
|
||||||
|
lastTimeString, _ := json.Marshal(lastTime)
|
||||||
|
return string(lastTimeString)
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTime := a.provider.WeChatGetSessionLastTime(userName)
|
||||||
|
|
||||||
|
lastTimeString, _ := json.Marshal(lastTime)
|
||||||
|
|
||||||
|
return string(lastTimeString)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) SetSessionLastTime(userName string, stamp int64, messageId string) string {
|
||||||
|
if a.provider == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
lastTime := &wechat.WeChatLastTime{
|
||||||
|
UserName: userName,
|
||||||
|
Timestamp: stamp,
|
||||||
|
MessageId: messageId,
|
||||||
|
}
|
||||||
|
err := a.provider.WeChatSetSessionLastTime(lastTime)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("WeChatSetSessionLastTime failed:", err.Error())
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) SetSessionBookMask(userName, tag, info string) string {
|
||||||
|
if a.provider == nil || userName == "" {
|
||||||
|
return "invaild params"
|
||||||
|
}
|
||||||
|
err := a.provider.WeChatSetSessionBookMask(userName, tag, info)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("WeChatSetSessionBookMask failed:", err.Error())
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) DelSessionBookMask(markId string) string {
|
||||||
|
if a.provider == nil || markId == "" {
|
||||||
|
return "invaild params"
|
||||||
|
}
|
||||||
|
|
||||||
|
err := a.provider.WeChatDelSessionBookMask(markId)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("WeChatDelSessionBookMask failed:", err.Error())
|
||||||
|
return err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) GetSessionBookMaskList(userName string) string {
|
||||||
|
if a.provider == nil || userName == "" {
|
||||||
|
return "invaild params"
|
||||||
|
}
|
||||||
|
markLIst, err := a.provider.WeChatGetSessionBookMaskList(userName)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("WeChatGetSessionBookMaskList failed:", err.Error())
|
||||||
|
_list := &wechat.WeChatBookMarkList{}
|
||||||
|
_listString, _ := json.Marshal(_list)
|
||||||
|
return string(_listString)
|
||||||
|
}
|
||||||
|
|
||||||
|
markLIstString, _ := json.Marshal(markLIst)
|
||||||
|
return string(markLIstString)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) SelectedDirDialog(title string) string {
|
||||||
|
dialogOptions := runtime.OpenDialogOptions{
|
||||||
|
Title: title,
|
||||||
|
}
|
||||||
|
selectedDir, err := runtime.OpenDirectoryDialog(a.ctx, dialogOptions)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("OpenDirectoryDialog:", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
if selectedDir == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return selectedDir
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) ExportWeChatDataByUserName(userName, path string) string {
|
||||||
|
if a.provider == nil || userName == "" || path == "" {
|
||||||
|
return "invaild params" + userName
|
||||||
|
}
|
||||||
|
|
||||||
|
if !utils.PathIsCanWriteFile(path) {
|
||||||
|
log.Println("PathIsCanWriteFile: " + path)
|
||||||
|
return "PathIsCanWriteFile: " + path
|
||||||
|
}
|
||||||
|
|
||||||
|
exPath := path + "\\" + "wechatDataBackup_" + userName
|
||||||
|
if _, err := os.Stat(exPath); err != nil {
|
||||||
|
os.MkdirAll(exPath, os.ModePerm)
|
||||||
|
} else {
|
||||||
|
return "path exist:" + exPath
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("ExportWeChatDataByUserName:", userName, exPath)
|
||||||
|
err := a.provider.WeChatExportDataByUserName(userName, exPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("WeChatExportDataByUserName failed:", err)
|
||||||
|
return "WeChatExportDataByUserName failed:" + err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
config := map[string]interface{}{
|
||||||
|
"exportpath": ".\\",
|
||||||
|
"userconfig": map[string]interface{}{
|
||||||
|
"defaultuser": a.defaultUser,
|
||||||
|
"users": []string{a.defaultUser},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
configJson, err := json.MarshalIndent(config, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
log.Println("MarshalIndent:", err)
|
||||||
|
return "MarshalIndent:" + err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
configPath := exPath + "\\" + "config.json"
|
||||||
|
err = os.WriteFile(configPath, configJson, os.ModePerm)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("WriteFile:", err)
|
||||||
|
return "WriteFile:" + err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
exeSrcPath, err := os.Executable()
|
||||||
|
if err != nil {
|
||||||
|
log.Println("Executable:", exeSrcPath)
|
||||||
|
return "Executable:" + err.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
exeDstPath := exPath + "\\" + "wechatDataBackup.exe"
|
||||||
|
log.Printf("Copy [%s] -> [%s]\n", exeSrcPath, exeDstPath)
|
||||||
|
_, err = utils.CopyFile(exeSrcPath, exeDstPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Println("CopyFile:", err)
|
||||||
|
return "CopyFile:" + err.Error()
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *App) GetAppIsShareData() bool {
|
||||||
|
if a.provider != nil {
|
||||||
|
return a.provider.IsShareData
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
Before Width: | Height: | Size: 7.7 KiB After Width: | Height: | Size: 28 KiB |
Before Width: | Height: | Size: 411 KiB After Width: | Height: | Size: 82 KiB |
33
changelog.md
@ -1,3 +1,36 @@
|
|||||||
|
## v1.2.4
|
||||||
|
1. 修复FileStorage\Image文件没有导出的问题
|
||||||
|
2. 增加图片定位到聊天位置的功能
|
||||||
|
3. 导出界面增加提示
|
||||||
|
|
||||||
|
## v1.2.3
|
||||||
|
1. 修改程序ICON
|
||||||
|
|
||||||
|
## v1.2.2
|
||||||
|
1. UI统一使用“思源黑体”字体
|
||||||
|
2. 修复导出分享会话时可执行文件可能拷贝错误的情况
|
||||||
|
3. 修复微信存储路径为网盘时,解密会失败的情况
|
||||||
|
|
||||||
|
## v1.2.0
|
||||||
|
1. 实现最后浏览位置记录和书签功能
|
||||||
|
2. 实现会话导出分享功能
|
||||||
|
3. 增加语音消息和通话消息的筛选
|
||||||
|
4. 实现单聊会话对话人位置调换功能
|
||||||
|
5. 添加繁体和英语表情的解析
|
||||||
|
6. 修复相近时间的消息可能会出现顺序不对的问题
|
||||||
|
|
||||||
|
## v1.1.0
|
||||||
|
1. 支持转账、通话、链接消息的显示
|
||||||
|
2. 支持名片、视频号、QQ音乐、小程序、定位等消息的显示
|
||||||
|
3. 支持直播、游戏消息、语音通话、视频通话等消息的显示
|
||||||
|
4. 解密前判断对数据库是否有读权限
|
||||||
|
5. system消息html格式转文本
|
||||||
|
|
||||||
|
## v1.0.7
|
||||||
|
1. 修复出现空数据库时不显示,聊天显示不全的问题
|
||||||
|
2. 解决安卓平板扫码登陆的情况下解密失败的问题
|
||||||
|
3. 移除lame和silk代码,改用mod的方式引入
|
||||||
|
|
||||||
## v1.0.6
|
## v1.0.6
|
||||||
1. 图片/视频查看重新实现,保持与微信一致
|
1. 图片/视频查看重新实现,保持与微信一致
|
||||||
2. 增加软件信息页面
|
2. 增加软件信息页面
|
||||||
|
BIN
frontend/dist/assets/110_怄火.896c41d9.png
vendored
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
frontend/dist/assets/112_左哼哼.5425438e.png
vendored
Normal file
After Width: | Height: | Size: 11 KiB |
BIN
frontend/dist/assets/113_哈欠.fe9c8521.png
vendored
Normal file
After Width: | Height: | Size: 13 KiB |
BIN
frontend/dist/assets/applet.ce6471b1.png
vendored
Normal file
After Width: | Height: | Size: 7.0 KiB |
BIN
frontend/dist/assets/channels.33204285.png
vendored
Normal file
After Width: | Height: | Size: 8.7 KiB |
BIN
frontend/dist/assets/channels_error.1d149df5.png
vendored
Normal file
After Width: | Height: | Size: 5.6 KiB |
BIN
frontend/dist/assets/favorite.1b38cfe5.png
vendored
Normal file
After Width: | Height: | Size: 4.7 KiB |
1
frontend/dist/assets/index.00f6955e.css
vendored
Normal file
533
frontend/dist/assets/index.8be36b27.js
vendored
Normal file
1
frontend/dist/assets/index.a11c4643.css
vendored
533
frontend/dist/assets/index.d5e97187.js
vendored
BIN
frontend/dist/assets/logo.8df6944e.png
vendored
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
frontend/dist/assets/map.b91d2cda.png
vendored
Normal file
After Width: | Height: | Size: 16 KiB |
BIN
frontend/dist/assets/music_note.02e237d9.png
vendored
Normal file
After Width: | Height: | Size: 7.1 KiB |
BIN
frontend/dist/assets/qq_music.b548e6a1.png
vendored
Normal file
After Width: | Height: | Size: 7.0 KiB |
BIN
frontend/dist/assets/red_packet.704bd303.png
vendored
Normal file
After Width: | Height: | Size: 91 KiB |
BIN
frontend/dist/assets/share.3d7abf39.png
vendored
Normal file
After Width: | Height: | Size: 4.1 KiB |
BIN
frontend/dist/assets/思源黑体-Normal.df5ff3ec.otf
vendored
Normal file
4
frontend/dist/index.html
vendored
@ -4,8 +4,8 @@
|
|||||||
<meta charset="UTF-8"/>
|
<meta charset="UTF-8"/>
|
||||||
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
<meta content="width=device-width, initial-scale=1.0" name="viewport"/>
|
||||||
<title>wechatDataBackup</title>
|
<title>wechatDataBackup</title>
|
||||||
<script type="module" crossorigin src="/assets/index.d5e97187.js"></script>
|
<script type="module" crossorigin src="/assets/index.8be36b27.js"></script>
|
||||||
<link rel="stylesheet" href="/assets/index.a11c4643.css">
|
<link rel="stylesheet" href="/assets/index.00f6955e.css">
|
||||||
</head>
|
</head>
|
||||||
<body >
|
<body >
|
||||||
<div id="root"></div>
|
<div id="root"></div>
|
||||||
|
2
go.mod
@ -13,6 +13,7 @@ require (
|
|||||||
github.com/shirou/gopsutil/v3 v3.24.2
|
github.com/shirou/gopsutil/v3 v3.24.2
|
||||||
github.com/spf13/viper v1.18.2
|
github.com/spf13/viper v1.18.2
|
||||||
github.com/wailsapp/wails/v2 v2.9.1
|
github.com/wailsapp/wails/v2 v2.9.1
|
||||||
|
golang.org/x/net v0.25.0
|
||||||
golang.org/x/sys v0.20.0
|
golang.org/x/sys v0.20.0
|
||||||
google.golang.org/protobuf v1.31.0
|
google.golang.org/protobuf v1.31.0
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1
|
||||||
@ -62,7 +63,6 @@ require (
|
|||||||
go.uber.org/multierr v1.9.0 // indirect
|
go.uber.org/multierr v1.9.0 // indirect
|
||||||
golang.org/x/crypto v0.23.0 // indirect
|
golang.org/x/crypto v0.23.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // 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
|
golang.org/x/text v0.15.0 // indirect
|
||||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
3
main.go
@ -51,6 +51,7 @@ func main() {
|
|||||||
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
|
BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1},
|
||||||
OnStartup: app.startup,
|
OnStartup: app.startup,
|
||||||
OnBeforeClose: app.beforeClose,
|
OnBeforeClose: app.beforeClose,
|
||||||
|
OnShutdown: app.shutdown,
|
||||||
Bind: []interface{}{
|
Bind: []interface{}{
|
||||||
app,
|
app,
|
||||||
},
|
},
|
||||||
@ -58,6 +59,6 @@ func main() {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
println("Error:", err.Error())
|
log.Println("Error:", err.Error())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,8 @@
|
|||||||
package utils
|
package utils
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/hex"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@ -8,10 +10,12 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/pkg/browser"
|
"github.com/pkg/browser"
|
||||||
"github.com/shirou/gopsutil/v3/disk"
|
"github.com/shirou/gopsutil/v3/disk"
|
||||||
|
"golang.org/x/net/html"
|
||||||
"golang.org/x/sys/windows/registry"
|
"golang.org/x/sys/windows/registry"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -150,3 +154,83 @@ func CopyFile(src, dst string) (int64, error) {
|
|||||||
|
|
||||||
return bytesWritten, nil
|
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 == "" {
|
||||||
|
// return ""
|
||||||
|
// }
|
||||||
|
|
||||||
|
if len(htmlStr) == 0 || 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)
|
||||||
|
}
|
||||||
|
@ -310,6 +310,7 @@ func exportWeChatVideoAndFile(info WeChatInfo, expPath string, progress chan<- s
|
|||||||
videoRootPath := info.FilePath + "\\FileStorage\\Video"
|
videoRootPath := info.FilePath + "\\FileStorage\\Video"
|
||||||
fileRootPath := info.FilePath + "\\FileStorage\\File"
|
fileRootPath := info.FilePath + "\\FileStorage\\File"
|
||||||
cacheRootPath := info.FilePath + "\\FileStorage\\Cache"
|
cacheRootPath := info.FilePath + "\\FileStorage\\Cache"
|
||||||
|
|
||||||
rootPaths := []string{videoRootPath, fileRootPath, cacheRootPath}
|
rootPaths := []string{videoRootPath, fileRootPath, cacheRootPath}
|
||||||
|
|
||||||
handleNumber := int64(0)
|
handleNumber := int64(0)
|
||||||
@ -326,6 +327,9 @@ func exportWeChatVideoAndFile(info WeChatInfo, expPath string, progress chan<- s
|
|||||||
go func() {
|
go func() {
|
||||||
for _, rootPath := range rootPaths {
|
for _, rootPath := range rootPaths {
|
||||||
log.Println(rootPath)
|
log.Println(rootPath)
|
||||||
|
if _, err := os.Stat(rootPath); err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
err := filepath.Walk(rootPath, func(path string, finfo os.FileInfo, err error) error {
|
err := filepath.Walk(rootPath, func(path string, finfo os.FileInfo, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("filepath.Walk:%v\n", err)
|
log.Printf("filepath.Walk:%v\n", err)
|
||||||
@ -399,43 +403,51 @@ func exportWeChatVideoAndFile(info WeChatInfo, expPath string, progress chan<- s
|
|||||||
func exportWeChatBat(info WeChatInfo, expPath string, progress chan<- string) {
|
func exportWeChatBat(info WeChatInfo, expPath string, progress chan<- string) {
|
||||||
progress <- "{\"status\":\"processing\", \"result\":\"export WeChat Dat start\", \"progress\": 21}"
|
progress <- "{\"status\":\"processing\", \"result\":\"export WeChat Dat start\", \"progress\": 21}"
|
||||||
datRootPath := info.FilePath + "\\FileStorage\\MsgAttach"
|
datRootPath := info.FilePath + "\\FileStorage\\MsgAttach"
|
||||||
fileInfo, err := os.Stat(datRootPath)
|
imageRootPath := info.FilePath + "\\FileStorage\\Image"
|
||||||
if err != nil || !fileInfo.IsDir() {
|
rootPaths := []string{datRootPath, imageRootPath}
|
||||||
progress <- fmt.Sprintf("{\"status\":\"error\", \"result\":\"%s error\"}", datRootPath)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
handleNumber := int64(0)
|
handleNumber := int64(0)
|
||||||
fileNumber := getPathFileNumber(datRootPath, ".dat")
|
fileNumber := int64(0)
|
||||||
|
for i := range rootPaths {
|
||||||
|
fileNumber += getPathFileNumber(rootPaths[i], ".dat")
|
||||||
|
}
|
||||||
|
log.Println("DatFileNumber ", fileNumber)
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
var reportWg sync.WaitGroup
|
var reportWg sync.WaitGroup
|
||||||
quitChan := make(chan struct{})
|
quitChan := make(chan struct{})
|
||||||
taskChan := make(chan [2]string, 100)
|
taskChan := make(chan [2]string, 100)
|
||||||
go func() {
|
go func() {
|
||||||
err = filepath.Walk(datRootPath, func(path string, finfo os.FileInfo, err error) error {
|
for i := range rootPaths {
|
||||||
if err != nil {
|
if _, err := os.Stat(rootPaths[i]); err != nil {
|
||||||
log.Printf("filepath.Walk:%v\n", err)
|
continue
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if !finfo.IsDir() && strings.HasSuffix(path, ".dat") {
|
err := filepath.Walk(rootPaths[i], func(path string, finfo os.FileInfo, err error) error {
|
||||||
expFile := expPath + path[len(info.FilePath):]
|
|
||||||
_, err := os.Stat(filepath.Dir(expFile))
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
os.MkdirAll(filepath.Dir(expFile), 0644)
|
log.Printf("filepath.Walk:%v\n", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !finfo.IsDir() && strings.HasSuffix(path, ".dat") {
|
||||||
|
expFile := expPath + path[len(info.FilePath):]
|
||||||
|
_, err := os.Stat(filepath.Dir(expFile))
|
||||||
|
if err != nil {
|
||||||
|
os.MkdirAll(filepath.Dir(expFile), 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
task := [2]string{path, expFile}
|
||||||
|
taskChan <- task
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
task := [2]string{path, expFile}
|
|
||||||
taskChan <- task
|
|
||||||
return nil
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println("filepath.Walk:", err)
|
||||||
|
progress <- fmt.Sprintf("{\"status\":\"error\", \"result\":\"%v\"}", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Println("filepath.Walk:", err)
|
|
||||||
progress <- fmt.Sprintf("{\"status\":\"error\", \"result\":\"%v\"}", err)
|
|
||||||
}
|
}
|
||||||
close(taskChan)
|
close(taskChan)
|
||||||
}()
|
}()
|
||||||
@ -445,7 +457,7 @@ func exportWeChatBat(info WeChatInfo, expPath string, progress chan<- string) {
|
|||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
for task := range taskChan {
|
for task := range taskChan {
|
||||||
_, err = os.Stat(task[1])
|
_, err := os.Stat(task[1])
|
||||||
if err == nil {
|
if err == nil {
|
||||||
atomic.AddInt64(&handleNumber, 1)
|
atomic.AddInt64(&handleNumber, 1)
|
||||||
continue
|
continue
|
||||||
@ -598,7 +610,7 @@ func GetWeChatInfo() (list *WeChatInfoList) {
|
|||||||
for _, f := range files {
|
for _, f := range files {
|
||||||
if strings.HasSuffix(f.Path, "\\Media.db") {
|
if strings.HasSuffix(f.Path, "\\Media.db") {
|
||||||
// fmt.Printf("opened %s\n", f.Path[4:])
|
// fmt.Printf("opened %s\n", f.Path[4:])
|
||||||
filePath := f.Path[4:]
|
filePath := f.Path
|
||||||
parts := strings.Split(filePath, string(filepath.Separator))
|
parts := strings.Split(filePath, string(filepath.Separator))
|
||||||
if len(parts) < 4 {
|
if len(parts) < 4 {
|
||||||
log.Println("Error filePath " + filePath)
|
log.Println("Error filePath " + filePath)
|
||||||
@ -697,6 +709,12 @@ func Is64BitProcess(pid uint32) (bool, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetWeChatKey(info *WeChatInfo) string {
|
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))
|
handle, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_VM_READ, false, uint32(info.ProcessID))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("Error opening process:", err)
|
log.Println("Error opening process:", err)
|
||||||
@ -741,6 +759,7 @@ func hasDeviceSybmol(buffer []byte) int {
|
|||||||
{'p', 'a', 'd', '-', 'a', 'n', 'd', 'r', 'o', 'i', 'd', 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00},
|
{'p', 'a', 'd', '-', 'a', 'n', 'd', 'r', 'o', 'i', 'd', 0x00, 0x00, 0x00, 0x00, 0x00, 0x0B, 0x00, 0x00, 0x00},
|
||||||
{'i', 'p', 'h', 'o', 'n', 'e', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00},
|
{'i', 'p', 'h', 'o', 'n', 'e', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x00, 0x00, 0x00},
|
||||||
{'i', 'p', 'a', 'd', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00},
|
{'i', 'p', 'a', 'd', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00},
|
||||||
|
{'O', 'H', 'O', 'S', 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00},
|
||||||
}
|
}
|
||||||
for _, syb := range sybmols {
|
for _, syb := range sybmols {
|
||||||
if index := bytes.Index(buffer, syb); index != -1 {
|
if index := bytes.Index(buffer, syb); index != -1 {
|
||||||
|
BIN
res/logo.png
Normal file
After Width: | Height: | Size: 470 KiB |
BIN
res/logo_128.png
Normal file
After Width: | Height: | Size: 8.2 KiB |
BIN
res/logo_256.png
Normal file
After Width: | Height: | Size: 28 KiB |
BIN
res/wechatQR.png
Normal file
After Width: | Height: | Size: 70 KiB |