Compare commits

...

156 Commits

Author SHA1 Message Date
叨叨
e85f9f2882
Optimize readme (#209)
* 1、修改加粗异常
2、英文Readme中免责声明改为英文
3、贡献者归类

* 删除重复部分

---------

Signed-off-by: 叨叨 <173192386@qq.com>
2025-04-30 01:52:32 +08:00
叨叨
1f627408da
优化英文README.md
1、修改加粗异常2、英文Readme中免责声明改为英文 3、贡献者归类 (#208)
2025-04-29 23:57:43 +08:00
xaoyaoo
8330cb974a UPDATE CHANGELOG.md 2025-03-20 08:32:13 +08:00
xaoyaoo
8b652d2d07 UPDATE CHANGELOG.md 2025-03-10 18:56:55 +08:00
xaoyaoo
e3817b2c0e add wx 3.9.12.51 2025-03-10 18:55:16 +08:00
xaoyaoo
4a63d6fa3e fix gen_change_log.py 2025-03-08 16:00:45 +08:00
xaoyaoo
79d8af07ef UPDATE CHANGELOG.md 2025-03-08 16:00:24 +08:00
xaoyaoo
0134299066 fix #178 2025-03-08 15:58:31 +08:00
xaoyaoo
79ae0b0edb fix #176 2025-03-08 15:57:48 +08:00
xaoyaoo
78265602bb update #178 2025-03-08 15:52:03 +08:00
xaoyaoo
7a0c7f384a add 3.9.12.45 2025-02-21 08:59:39 +08:00
xaoyaoo
42679aa5c6 add 3.9.12.45 2025-02-21 08:59:25 +08:00
xaoyaoo
881b0e539e add wx 3.9.12.37 2025-02-03 20:43:07 +08:00
xaoyaoo
36a9e4f1aa add wx 3.9.12.37 2025-02-03 20:42:30 +08:00
xaoyaoo
e2f193c616 add wx 3.9.12.31 2025-02-03 20:23:57 +08:00
xaoyaoo
c03fd47637 Merge remote-tracking branch 'origin' 2025-01-14 09:50:31 +08:00
xaoyaoo
ef32e935b8 add wx 3.9.12.31 2025-01-14 09:49:06 +08:00
xaoyaoo
983610f848 add wx 3.9.12.31 2025-01-14 09:47:53 +08:00
SasakiSaki
0ac4c9cfbb
新增消息分类 (#162)
* 新增消息分类
Signed-off-by: SasakiSaki <192608617@qq.com>
2025-01-10 23:38:46 +08:00
xaoyaoo
fcab5ad5e1 fix 修改flask启动方式 2024-12-29 02:03:21 +08:00
xaoyaoo
6a3d0797fb fix 修改flask启动方式 2024-12-29 01:38:12 +08:00
xaoyaoo
ee007f5ab1 UPDATE CHANGELOG.md 2024-10-19 09:44:38 +08:00
xaoyaoo
6d8ff36164 fix 2024-10-19 09:44:21 +08:00
xaoyaoo
654ccdd177 实时消息增加中文路径支持 2024-10-19 09:38:55 +08:00
xaoyaoo
48bdf80ac4 UPDATE CHANGELOG.md 2024-10-16 02:37:53 +08:00
xaoyaoo
e0d0703337 fix 2024-10-16 02:37:38 +08:00
xaoyaoo
4c37b577a6 modify log fmt 2024-10-11 14:52:35 +08:00
xaoyaoo
001364b3c6 fix tag查询结果去重 2024-10-09 15:15:39 +08:00
xaoyaoo
9acdb94c07 完善收藏的类型转换体系 2024-10-09 14:40:50 +08:00
xaoyaoo
c870b04dd9 fix #143 2024-10-07 23:31:27 +08:00
xaoyaoo
82bf28e591 UPDATE CHANGELOG.md 2024-10-07 15:30:10 +08:00
xaoyaoo
235a35cebd 增加api文档说明 2024-10-07 15:29:52 +08:00
xaoyaoo
5cd4f10328 增加api文档说明 2024-10-07 14:50:24 +08:00
xaoyaoo
cbebf018d4 fix 2024-10-06 19:00:18 +08:00
xaoyaoo
4079545ec2 UPDATE CHANGELOG.md 2024-10-03 22:20:30 +08:00
xaoyaoo
2acc3fb621 UPDATE WXOFFS 3.9.12.17 2024-10-03 22:20:13 +08:00
xaoyaoo
d587034d3d UPDATE WXOFFS 3.9.12.17 2024-10-03 22:20:03 +08:00
xaoyaoo
f07e65768c fix CE获取基址.md 2024-09-24 23:52:00 +08:00
xaoyaoo
51d445891f add 注释 2024-09-17 20:01:29 +08:00
xaoyaoo
b9bd48c3b7 UPDATE CHANGELOG.md 2024-09-11 12:16:32 +08:00
xaoyaoo
91c36ca300 add wx 3.9.12.15 2024-09-11 12:15:54 +08:00
xaoyaoo
e0b891d698 add wx 3.9.12.15 2024-09-11 12:14:55 +08:00
xaoyaoo
4a61cda9fa update UserGuide.md 2024-09-09 09:16:16 +08:00
xaoyaoo
5a479ba514 UPDATE CHANGELOG.md 2024-09-09 09:13:36 +08:00
xaoyaoo
31e69a6fb5 fix 2024-09-09 09:13:31 +08:00
xaoyaoo
be3ba1a712 UPDATE CHANGELOG.md 2024-09-09 09:13:12 +08:00
xaoyaoo
b95fb68cd9 UPDATE CHANGELOG.md 2024-09-09 09:11:40 +08:00
xaoyaoo
72cb404e35 群聊增加群成员显示 2024-09-08 08:52:41 +08:00
xaoyaoo
65d824cf42 计划增加自动推送到gitee 2024-09-08 08:46:14 +08:00
xaoyaoo
a385434c12 UPDATE CHANGELOG.md 2024-09-08 08:45:32 +08:00
xaoyaoo
d35edd0c6c UPDATE CHANGELOG.md 2024-09-06 11:55:23 +08:00
xaoyaoo
75d9081217 UPDATE CHANGELOG.md 2024-09-06 11:53:21 +08:00
xaoyaoo
40a3b60c25 群聊增加群成员显示 2024-09-06 11:52:56 +08:00
xaoyaoo
4decd8c6e7 群聊增加群成员显示 2024-09-06 11:52:30 +08:00
xaoyaoo
bc53238ade 修改注释 2024-09-06 10:05:30 +08:00
xaoyaoo
d0e004deb4 M CE获取基址.md 2024-09-06 09:20:25 +08:00
xaoyaoo
caade0cb20 UPDATE CHANGELOG.md 2024-09-03 15:19:13 +08:00
xaoyaoo
9e3c7309c0 fix 联系人搜索bug 2024-09-03 15:18:57 +08:00
xaoyaoo
3f2b341210 fix 联系人搜索bug 2024-09-03 15:16:41 +08:00
xaoyaoo
0314cf4107 fix 联系人搜索bug 2024-09-03 15:16:30 +08:00
xaoyaoo
f698e4d348 add log to file 2024-09-03 14:36:13 +08:00
xaoyaoo
e17888cd80 add log to file 2024-09-03 14:35:49 +08:00
xaoyaoo
e75465d93c fix 2024-09-02 13:32:17 +08:00
xaoyaoo
91b7aa34cb fix 2024-08-30 10:32:42 +08:00
xaoyaoo
5307297144 fix 2024-08-28 17:51:45 +08:00
xaoyaoo
c5b9c009df fix 2024-08-28 17:46:21 +08:00
xaoyaoo
644537b9b6 fix 群聊list 2024-08-26 20:00:42 +08:00
xaoyaoo
b7d4841a74 fix 群聊list 2024-08-26 11:51:25 +08:00
xaoyaoo
d379e4e953 fix #125 2024-08-21 12:19:41 +08:00
xaoyaoo
cf92003e62 UPDATE CHANGELOG.md 2024-08-20 17:38:35 +08:00
xaoyaoo
bbee43cc9f fix DIl load failed while importing pydantic_core: 2024-08-20 17:38:10 +08:00
xaoyaoo
3cc599c231 fix DIl load failed while importing pydantic_core: 2024-08-20 17:36:41 +08:00
xaoyaoo
1002c2368a fix DIl load failed while importing pydantic_core: 2024-08-20 17:35:39 +08:00
xaoyaoo
ca6c6a022a update test 2024-08-19 10:59:52 +08:00
xaoyaoo
60ae50eeac fix 2024-08-19 10:56:14 +08:00
xaoyaoo
00ecf3f458 UPDATE CHANGELOG.md 2024-08-18 20:30:41 +08:00
xaoyaoo
c4ec6386c7 fix dbshow #124 2024-08-18 20:30:17 +08:00
xaoyaoo
461e922dcf fix 2024-08-18 15:29:46 +08:00
xaoyaoo
1ec4bf5965 fix 2024-08-18 15:29:14 +08:00
xaoyaoo
da47822c92 UPDATE CHANGELOG.md 2024-08-18 15:18:41 +08:00
xaoyaoo
8887b381b9 fix 部分图片无法读取 2024-08-18 15:18:22 +08:00
xaoyaoo
4da72a8522 fix 部分图片无法读取 2024-08-18 14:37:36 +08:00
xaoyaoo
a61a3e7dfe get_msgs 允许多个wxid 2024-08-18 13:59:32 +08:00
xaoyaoo
b3dd6a0715 get_msgs 允许多个wxid 2024-08-18 13:55:04 +08:00
xaoyaoo
82363ef384 UPDATE CHANGELOG.md 2024-08-18 13:33:39 +08:00
xaoyaoo
799b28a668 import 优化 2024-08-18 13:33:08 +08:00
xaoyaoo
b799cb1384 fix import时候无法自动处理data 2024-08-18 13:23:06 +08:00
xaoyaoo
d917f3f581 fix import时候无法自动处理data 2024-08-18 13:14:29 +08:00
xaoyaoo
b4cc73e495 fix import时候无法自动处理data 2024-08-18 12:09:39 +08:00
xaoyaoo
dab025bb7b fix 2024-08-18 10:58:49 +08:00
xaoyaoo
0c56e20c9d rename core_db_type 2024-08-18 10:50:20 +08:00
xaoyaoo
9f1a7d3899 fix 单用户使用上次数据报错问题 2024-08-18 00:24:47 +08:00
xaoyaoo
0f0bc5b19c UPDATE CHANGELOG.md 2024-08-18 00:24:24 +08:00
xaoyaoo
154b7c5286 fix 单用户使用上次数据报错问题 2024-08-18 00:23:44 +08:00
xaoyaoo
08bb49dc9d fix 无限递归问题 2024-08-18 00:04:39 +08:00
xaoyaoo
3d9624f321 fix #123 2024-08-18 00:03:55 +08:00
xaoyaoo
09d436a83c update README.md 2024-08-17 22:44:32 +08:00
xaoyaoo
e771ec1c32 fix 2024-08-17 17:28:57 +08:00
xaoyaoo
a4fb7508b3 fix 2024-08-17 16:36:58 +08:00
xaoyaoo
a1da964849 UPDATE CHANGELOG.md 2024-08-17 14:11:17 +08:00
xaoyaoo
d51c662659 加快web页面加载数据,重置部分api,调整server为fastapi 2024-08-17 14:06:43 +08:00
xaoyaoo
5b3f26c40c 加快web页面加载数据,重置部分api,调整server为fastapi 2024-08-17 14:04:46 +08:00
xaoyaoo
85362627ea fix 2024-08-17 14:04:03 +08:00
xaoyaoo
df7eecdaa4 fix build exe error 2024-08-17 13:58:23 +08:00
xaoyaoo
0c727dc34f fix build exe error 2024-08-17 13:51:44 +08:00
xaoyaoo
33da8c4b78 该flask为fastapi,速度更快 2024-08-17 12:25:13 +08:00
xaoyaoo
3bde268c55 UPDATE CHANGELOG.md 2024-08-17 12:18:23 +08:00
xaoyaoo
b5fee91e28 该flask为fastapi,速度更快 2024-08-17 12:07:29 +08:00
xaoyaoo
e5a0754e7f 该flask为fastapi,速度更快 2024-08-17 12:00:51 +08:00
xaoyaoo
a2ffebf49e 该flask为fastapi,速度更快 2024-08-17 11:53:03 +08:00
xaoyaoo
0b937a2f11 该flask为fastapi,速度更快 2024-08-17 11:51:57 +08:00
xaoyaoo
81832a2a99 add export html 2024-08-13 23:54:21 +08:00
xaoyaoo
36fcb2ac44 fix and add export html 2024-08-13 22:59:46 +08:00
xaoyaoo
37293a5af2 add Sns 2024-08-13 19:32:32 +08:00
xaoyaoo
3ea91fb962 add Sns 2024-08-13 19:29:36 +08:00
xaoyaoo
2539b97fb9 add Sns 2024-08-13 19:13:18 +08:00
xaoyaoo
d0e75a9f15 fix 2024-08-13 18:29:46 +08:00
xaoyaoo
bd4a880d7a 增加注释,增加自定义实时合并数据库路径 2024-08-13 18:02:37 +08:00
xaoyaoo
8b21c1c6b0 增加注释,增加自定义实时合并数据库路径 2024-08-13 18:02:24 +08:00
xaoyaoo
ccbcf5878c 增加注释,增加自定义实时合并数据库路径 2024-08-13 17:47:25 +08:00
xaoyaoo
1922c1da39 Media_add_index 2024-08-13 17:37:59 +08:00
xaoyaoo
4fe4bfd47c Media_add_index 2024-08-13 17:37:07 +08:00
xaoyaoo
e83f58163b 增加注释说明 2024-08-13 17:31:06 +08:00
xaoyaoo
7b3ad34f81 UPDATE CHANGELOG.md 2024-08-13 17:14:01 +08:00
xaoyaoo
7eef77f07a UPDATE CHANGELOG.md 2024-08-13 13:22:14 +08:00
xaoyaoo
99c273ea11 fix 2024-08-13 13:21:57 +08:00
xaoyaoo
3a6f070410 fix 2024-08-13 13:20:03 +08:00
xaoyaoo
0e675f6629 分离检查数据库中表是否存在,在每个函数运行前检查表是否存在 2024-08-13 12:53:53 +08:00
xaoyaoo
bc3c9baa99 (分享)卡片式链接 增加extra,包含更多数据 2024-08-11 23:01:45 +08:00
xaoyaoo
8727c28459 (分享)卡片式链接 增加extra,包含更多数据 2024-08-11 23:01:26 +08:00
xaoyaoo
3470b6b71c UPDATE CHANGELOG.md 2024-08-11 22:48:34 +08:00
xaoyaoo
ddbd0dba92 fix export 2024-08-11 22:48:03 +08:00
xaoyaoo
56d8f9b4dd fix export 2024-08-11 22:46:40 +08:00
xaoyaoo
df409fa39b 新增个性签名词云 2024-08-11 18:04:35 +08:00
xaoyaoo
647f2dc2cc 新增个性签名词云 2024-08-11 18:04:22 +08:00
xaoyaoo
c50a31f814 UPDATE CHANGELOG.md 2024-08-11 13:43:37 +08:00
xaoyaoo
8ae6dfdbe1 fix build error 2024-08-11 13:43:24 +08:00
xaoyaoo
6c1cca2966 fix 2024-08-11 13:42:39 +08:00
xaoyaoo
c1b638a87b fix bug;略微调整UI 2024-08-11 13:37:43 +08:00
xaoyaoo
ff8c54b55c UPDATE CHANGELOG.md 2024-08-11 13:30:22 +08:00
xaoyaoo
9aa3601973 添加注释 2024-08-11 13:27:19 +08:00
xaoyaoo
5a96564828 fix 2024-08-11 11:57:19 +08:00
xaoyaoo
0c020ec602 fix 2024-08-11 11:17:57 +08:00
xaoyaoo
af6e866a18 fix 2024-08-11 11:17:45 +08:00
xaoyaoo
49db68bb8e fix 2024-08-11 11:14:29 +08:00
xaoyaoo
48f977736d fix 2024-08-11 11:07:22 +08:00
xaoyaoo
be4075df80 fix 2024-08-11 11:05:38 +08:00
xaoyaoo
472611153a fix 2024-08-11 10:56:16 +08:00
xaoyaoo
9a74e418ca fix 2024-08-11 10:54:41 +08:00
xaoyaoo
570e498057 fix 2024-08-11 10:39:07 +08:00
xaoyaoo
86886f50f2 实时消息增加工具路径设置 2024-08-10 23:24:25 +08:00
xaoyaoo
ee074a568a 实时消息增加工具路径设置 2024-08-10 23:19:28 +08:00
xaoyaoo
11921c1381 新标签页打开链接 2024-08-10 19:38:43 +08:00
xaoyaoo
7f316296b0 清理2.0的ui文件 2024-08-10 19:04:17 +08:00
xaoyaoo
fd642b4de0 清理2.0的ui文件 2024-08-10 19:03:38 +08:00
xaoyaoo
17c9bacc73 UPDATE CHANGELOG.md 2024-08-07 21:59:02 +08:00
53 changed files with 1759 additions and 1497 deletions

42
.github/workflows/auto-sync-gitee.yml vendored Normal file
View File

@ -0,0 +1,42 @@
#on:
# push:
# branches: [master]
#name: Mirror GitHub Repos to Gitee
#jobs:
# run:
# name: Sync-GitHub-to-Gitee
# runs-on: ubuntu-latest
# steps:
# - name: Mirror the Github repos to Gitee.
# uses: Yikun/hub-mirror-action@master
# with:
# src: github/xaoyaoo
# dst: gitee/xaoyaoo
# dst_key: ${{ secrets.GITEE_PRIVATE_KEY }}
# dst_token: ${{ secrets.GITEE_TOKEN }}
# force_update: true
# src_account_type: org
# dst_account_type: user
# mappings: "dashboard=>dashboards"
# static_list: "trader"
# cache_path: /github/workspace/hub-mirror-cache
name: Hello World Action
on:
push:
branches: [ main ] # 触发条件:当主分支有新的推送时
jobs:
hello-job:
runs-on: ubuntu-latest # 运行环境:最新的 Ubuntu 系统
steps:
- name: Checkout Repository
uses: actions/checkout@v3 # 检出代码
- name: Print Hello Message
run: echo "Hello, world!" # 执行命令,打印消息
- name: Print Date
run: date # 执行命令,打印当前日期

4
.gitignore vendored
View File

@ -26,4 +26,6 @@ node_modules
.DS_Store
dist
dist-ssr
*.local
*.local
/pywxdump/ui/web/*
/pywxdump/ui/web/assets/*

View File

@ -38,8 +38,7 @@ QQ GROUP[276392799](https://s.xaoyo.top/gOLUDl) or [276392799](https://s.xaoy
#### 2.1 Core
* (1) Get the **base address offset
** of WeChat nickname, WeChat account, WeChat phone number, WeChat email, and WeChat KEY
* (1) Get the **base address offset** of WeChat nickname, WeChat account, WeChat phone number, WeChat email, and WeChat KEY
* (2) Get the WeChat nickname, WeChat account, WeChat phone number, WeChat email, WeChat KEY, WeChat original ID (wxid_******), and WeChat folder path of the currently logged-in WeChat
* (3) Decrypt WeChat database based on key
* (4) Combine multiple types of databases for unified viewing
@ -71,12 +70,12 @@ QQ GROUP[276392799](https://s.xaoyo.top/gOLUDl) or [276392799](https://s.xaoy
## 3. Update plan
* 1.Analyze chat logs of each person and generate word clouds.
* 2.Analyze the number of chats per person per day and generate a line chart (day-number of chats)
* 3.Analyze the monthly and annual chat volume of different people and generate a line chart
* 4.Generate annual visualization reports
* ~~2.Analyze the number of chats per person per day and generate a line chart (day-number of chats)~~
* ~~3.Analyze the monthly and annual chat volume of different people and generate a line chart~~
* ~~4.Generate annual visualization reports~~
* 8.Increase support for enterprise WeChat
* 12.Viewing and backing up of the circle of friends
* 13.Clean up WeChat storage space and reduce the space occupied by WeChat (hopefully by selecting a person or group and finding out the media files involved in the chat logs of this group, such as pictures, videos, files, voice recordings, etc., and selectively (such as time periods) or batch-wise clearing them from the computer's cache by group conversation.)
* ~~13.Clean up WeChat storage space and reduce the space occupied by WeChat (hopefully by selecting a person or group and finding out the media files involved in the chat logs of this group, such as pictures, videos, files, voice recordings, etc., and selectively (such as time periods) or batch-wise clearing them from the computer's cache by group conversation.)~~
* 14.Automatically send messages to specified people through UI control
## 4. Other
@ -108,7 +107,7 @@ QQ GROUP[276392799](https://s.xaoyo.top/gOLUDl) or [276392799](https://s.xaoy
* If you want to modify the UI, clone the [wx_dump_web](https://github.com/xaoyaoo/wxdump_web) and modify it as needed (the UI is developed using VUE+ElementUI)
】:
note】:
* For obtaining the base address using cheat engine, refer to [CE obtaining base address.md](https://github.com/xaoyaoo/PyWxDump/tree/master/doc/CE获取基址.md)
(This method can be replaced by the `wxdump bias` command, and is only used for learning principles.)
@ -118,9 +117,7 @@ QQ GROUP[276392799](https://s.xaoyo.top/gOLUDl) or [276392799](https://s.xaoy
### 1. Purpose of use
* This project is only for learning and communication purposes, **please do not use it for illegal purposes**, **please
do not use it for illegal purposes**, **please do not use it for illegal purposes
**, otherwise the consequences will be borne by yourself.
* This project is only for learning and communication purposes, **please do not use it for illegal purposes**, **please do not use it for illegal purposes**, **please do not use it for illegal purposes**, otherwise the consequences will be borne by yourself.
* Users understand and agree that any violation of laws and regulations, infringement of the legitimate rights and interests of others, is unrelated to this project and its developers, and the consequences are borne by the user themselves.
### 2. Usage Period
@ -152,45 +149,13 @@ QQ GROUP[276392799](https://s.xaoyo.top/gOLUDl) or [276392799](https://s.xaoy
* Users are requested to carefully read and understand all contents of this disclaimer, and ensure that they strictly comply with relevant regulations when using this project.
# Ⅳ. 免责声明(非常重要!!!!!!!)
# Ⅳ. Acknowledgments
### 1. 使用目的
[![PyWxDump CONTRIBUTORS](https://contrib.rocks/image?repo=xaoyaoo/PyWxDump)](https://github.com/xaoyaoo/PyWxDump/graphs/contributors)
* 本项目仅供学习交流使用,**请勿用于非法用途****请勿用于非法用途****请勿用于非法用途**,否则后果自负。
* 用户理解并同意,任何违反法律法规、侵犯他人合法权益的行为,均与本项目及其开发者无关,后果由用户自行承担。
UI CONTRIBUTORS:
### 2. 使用期限
* 您应该在下载保存编译使用本项目的24小时内删除本项目的源代码和编译出的程序超出此期限的任何使用行为一概与本项目及其开发者无关。
### 3. 操作规范
* 本项目仅允许在授权情况下对数据库进行备份与查看,严禁用于非法目的,否则自行承担所有相关责任;用户如因违反此规定而引发的任何法律责任,将由用户自行承担,与本项目及其开发者无关。
* 严禁用于窃取他人隐私,严禁用于窃取他人隐私,严禁用于窃取他人隐私,否则自行承担所有相关责任。
* 严禁进行二次开发,严禁进行二次开发,严禁进行二次开发,否则自行承担所有相关责任。
### 4. 免责声明接受
* 下载、保存、进一步浏览源代码或者下载安装、编译使用本程序,表示你同意本警告,并承诺遵守它;
### 5. 禁止用于非法测试或渗透
* 禁止利用本项目的相关技术从事非法测试或渗透,禁止利用本项目的相关代码或相关技术从事任何非法工作,如因此产生的一切不良后果与本项目及其开发者无关。
* 任何因此产生的不良后果,包括但不限于数据泄露、系统瘫痪、侵犯隐私等,均与本项目及其开发者无关,责任由用户自行承担。
### 6. 免责声明修改
* 本免责声明可能根据项目运行情况和法律法规的变化进行修改和调整。用户应定期查阅本页面以获取最新版本的免责声明,使用本项目时应遵守最新版本的免责声明。
### 7. 其他
* 除本免责声明规定外,用户在使用本项目过程中应遵守相关的法律法规和道德规范。对于因用户违反相关规定而引发的任何纠纷或损失,本项目及其开发者不承担任何责任。
* 请用户慎重阅读并理解本免责声明的所有内容,确保在使用本项目时严格遵守相关规定。
# . Acknowledgments
[![PyWxDump CONTRIBUTORS](https://contrib.rocks/image?repo=xaoyaoo/PyWxDump)](https://github.com/xaoyaoo/PyWxDump/graphs/contributors)[![UI CONTRIBUTORS](https://contrib.rocks/image?repo=xaoyaoo/wxdump_web)](https://github.com/xaoyaoo/wxdump_web/graphs/contributors)
[![UI CONTRIBUTORS](https://contrib.rocks/image?repo=xaoyaoo/wxdump_web)](https://github.com/xaoyaoo/wxdump_web/graphs/contributors)
otherContributors:

View File

@ -1,3 +1,5 @@
## 注:本方法仅用于提供`pywxdump`的基址获取方式的原理。如果需要快捷获取基址,请执行`wxdump bias` + 各种参数。详细见命令说明或者可以使用图形界面启动wxdump.exe,选择实用工具,即可看到偏移获取,输入参数即可)
### 如何通过CE附加进程
1. 打开CE >> 选择左上角放大镜按钮 >> 选择微信进程 >> 选择附加到进程

View File

@ -1,6 +1,193 @@
## v3.1.13.(待发布)
## v3.1.46.(待发布)
-
- UPDATE CHANGELOG.md
## v3.1.45
- add wx 3.9.12.51
- UPDATE CHANGELOG.md
- fix gen_change_log.py
## v3.1.44
- fix #176
- fix #178
- update #178
## v3.1.43
- add 3.9.12.45
- add wx 3.9.12.37
## v3.1.42
- add wx 3.9.12.37
## v3.1.41, tag: v3.1.40, tag: v3.1.39
- 新增消息分类 (#162)
- fix 修改flask启动方式
- add wx 3.9.12.31
- UPDATE CHANGELOG.md
- Merge remote-tracking branch 'origin'
## v3.1.38
- fix
- 实时消息增加中文路径支持
- UPDATE CHANGELOG.md
## v3.1.37
- fix
- 完善收藏的类型转换体系
- fix tag查询结果去重
- modify log fmt
## v3.1.36
- fix #143
- UPDATE CHANGELOG.md
## v3.1.35
- fix
- 增加api文档说明
- UPDATE CHANGELOG.md
## v3.1.34
- add 注释
- fix CE获取基址.md
- UPDATE CHANGELOG.md
- UPDATE WXOFFS 3.9.12.17
## v3.1.33
- fix
- 群聊增加群成员显示
- 计划增加自动推送到gitee
- add wx 3.9.12.15
- UPDATE CHANGELOG.md
- update UserGuide.md
## v3.1.32
- 修改注释
- 群聊增加群成员显示
- M CE获取基址.md
- UPDATE CHANGELOG.md
## v3.1.31
- fix 联系人搜索bug
## v3.1.30
- fix
- add log to file
## v3.1.29
- fix
- fix 群聊list
## v3.1.28
- fix #125
- UPDATE CHANGELOG.md
## v3.1.27
- fix
- update test
- UPDATE CHANGELOG.md
- fix DIl load failed while importing pydantic_core:
## v3.1.26
- fix dbshow #124
## v3.1.25
- fix
- UPDATE CHANGELOG.md
## v3.1.24
- fix 部分图片无法读取
- get_msgs 允许多个wxid
- UPDATE CHANGELOG.md
## v3.1.23
- fix
- import 优化
- rename core_db_type
- fix import时候无法自动处理data
## v3.1.22
- fix 单用户使用上次数据报错问题
- UPDATE CHANGELOG.md
## v3.1.21
- fix
- fix #123
- fix 无限递归问题
- update README.md
- UPDATE CHANGELOG.md
## v3.1.20
- fix
- 增加注释说明
- add Sns
- Media_add_index
- add export html
- UPDATE CHANGELOG.md
- fix build exe error
- 增加注释,增加自定义实时合并数据库路径
- 该flask为fastapi速度更快
- fix and add export html
- 加快web页面加载数据重置部分api调整server为fastapi
## v3.1.18
- fix
- 分离检查数据库中表是否存在,在每个函数运行前检查表是否存在
## v3.1.17
- UPDATE CHANGELOG.md
- (分享)卡片式链接 增加extra包含更多数据
## v3.1.16
- 新增个性签名词云
- fix export
- UPDATE CHANGELOG.md
## v3.1.15
- fix
- fix build error
## v3.1.14
- fix
- 添加注释
- 新标签页打开链接
- 清理2.0的ui文件
- 实时消息增加工具路径设置
- fix bug略微调整UI
- UPDATE CHANGELOG.md
## v3.1.13
- fix
- UPDATE CHANGELOG.md
## v3.1.12
@ -259,7 +446,6 @@
## v3.0.15
- 增加合并数据库的容错
- (backup/master) 增加合并数据库的容错
## v3.0.14

View File

@ -44,7 +44,7 @@ QQ交流群[276392799](https://s.xaoyo.top/gOLUDl) or [276392799](https://s.x
* 4合并多种类型数据库方便统一查看
#### 2.2 扩展功能
[README_EN.md](README_EN.md)
* 1通过web查看聊天记录
* 2支持导出聊天记录为html、csv,备份微信聊天记录
* 3远程查看微信聊天记录必须网络可达例如局域网
@ -70,12 +70,12 @@ QQ交流群[276392799](https://s.xaoyo.top/gOLUDl) or [276392799](https://s.x
## 3. 更新计划
* 1.每个人聊天记录分析,生成词云。
* 2.分析每个人每天的聊天数量,生成折线图(天-聊天数量)
* 3.分析不同的人的月聊天数量,年聊天数量,生成折线图
* 4.生成年度可视化报告
* ~~2.分析每个人每天的聊天数量,生成折线图(天-聊天数量)~~
* ~~3.分析不同的人的月聊天数量,年聊天数量,生成折线图~~
* ~~4.生成年度可视化报~~
* 8.增加企业微信的支持
* 12.朋友圈的查看与备份
* 13.微信存储空间清理,减少微信占用空间(能通过选择某个人或群,把这群里的聊天记录中涉及的图片、视频、文件、语音等的媒体文件找出来,以群对话为单位有选择性的(比如时间段)或按群会话批量从电脑的缓存中清除。)
* ~~13.微信存储空间清理,减少微信占用空间(能通过选择某个人或群,把这群里的聊天记录中涉及的图片、视频、文件、语音等的媒体文件找出来,以群对话为单位有选择性的(比如时间段)或按群会话批量从电脑的缓存中清除。)~~
* 14.通过UI控制自动给指定人发送消息
## 4. 其他

View File

@ -71,12 +71,12 @@ QQ GROUP[276392799](https://s.xaoyo.top/gOLUDl) or [276392799](https://s.xaoy
## 3. Update plan
* 1.Analyze chat logs of each person and generate word clouds.
* 2.Analyze the number of chats per person per day and generate a line chart (day-number of chats)
* 3.Analyze the monthly and annual chat volume of different people and generate a line chart
* 4.Generate annual visualization reports
* ~~2.Analyze the number of chats per person per day and generate a line chart (day-number of chats)~~
* ~~3.Analyze the monthly and annual chat volume of different people and generate a line chart~~
* ~~4.Generate annual visualization reports~~
* 8.Increase support for enterprise WeChat
* 12.Viewing and backing up of the circle of friends
* 13.Clean up WeChat storage space and reduce the space occupied by WeChat (hopefully by selecting a person or group and finding out the media files involved in the chat logs of this group, such as pictures, videos, files, voice recordings, etc., and selectively (such as time periods) or batch-wise clearing them from the computer's cache by group conversation.)
* ~~13.Clean up WeChat storage space and reduce the space occupied by WeChat (hopefully by selecting a person or group and finding out the media files involved in the chat logs of this group, such as pictures, videos, files, voice recordings, etc., and selectively (such as time periods) or batch-wise clearing them from the computer's cache by group conversation.)~~
* 14.Automatically send messages to specified people through UI control
## 4. Other

View File

@ -29,7 +29,7 @@ pip install -U pywxdump
#### 1.2 从源码安装(安装最新版)
```shell script
pip install -U git+git://github.com/xaoyaoo/PyWxDump.git # 该方法无法安装网页图形界面
pip install -U git+git://github.com/xaoyaoo/PyWxDump.git # 该方法无法安装网页图形界面会导致浏览器显示页面无法打开显示404
```

View File

@ -227,23 +227,33 @@ FTS 这一前缀了——这代表的是搜索时所需的索引。
| 1 | 0 | 文本 |
| 3 | 0 | 图片 |
| 34 | 0 | 语音 |
| 37 | 0 | 打招呼, 加好友的时候输入的 `我是某某某` 这一句话 |
| 42 | 0 | 向别人推荐自己的好友 |
| 43 | 0 | 视频 |
| 47 | 0 | 动画表情(第三方开发的表情包) |
| 49 | 1 | 类似文字消息而不一样的消息目前只见到一个阿里云盘的邀请注册是这样的。估计和57子类的情况一样 |
| 48 | 0 | 地图定位 |
| 49 | 1 | 类似文字消息而不一样的消息目前只见到一个阿里云盘的邀请注册是这样的还有飞书日程。估计和57子类的情况一样 |
| 49 | 4 | 分享 Bilibili 视频 |
| 49 | 5 | 卡片式链接CompressContent 中有标题、简介等BytesExtra 中有本地缓存的封面路径 |
| 49 | 6 | 文件CompressContent 中有文件名和下载链接但不会读BytesExtra 中有本地保存的路径 |
| 49 | 8 | 用户上传的 GIF 表情CompressContent 中有CDN链接不过似乎不能直接访问下载 |
| 49 | 19 | 合并转发的聊天记录CompressContent 中有详细聊天记录BytesExtra 中有图片视频等的缓存 |
| 49 | 33/36 | 分享的小程序CompressContent 中有卡片信息BytesExtra 中有封面缓存位置 |
| 49 | 50 | 微视频 |
| 49 | 51 | 分享朋友圈动态 |
| 49 | 53 | 接龙 |
| 49 | 57 | 带有引用的文本消息(这种类型下 StrContent 为空,发送和引用的内容均在 CompressContent 中) |
| 49 | 63 | 视频号直播或直播回放等 |
| 49 | 76 | 分享歌曲 |
| 49 | 87 | 群公告 |
| 49 | 88 | 视频号直播或直播回放等 |
| 49 | 2000 | 转账消息(包括发出、接收、主动退还) |
| 49 | 2003 | 赠送红包封面 |
| 50 | 0 | 语音通话 |
| 65 | 0 | 朋友推荐消息 |
| 10000 | 0 | 系统通知(居中出现的那种灰色文字) |
| 10000 | 4 | 拍一拍 |
| 10000 | 8000 | 系统通知(特别包含你邀请别人加入群聊) |
## 更多内容查看:
https://blog.csdn.net/weixin_44495599/article/details/130030359
https://blog.csdn.net/weixin_44495599/article/details/130030359

View File

@ -411,5 +411,47 @@
93700888,
0,
93702352
],
"3.9.12.15": [
93813544,
93814880,
93813352,
0,
93814816
],
"3.9.12.17": [
93834984,
93836320,
93834792,
0,
93836256
],
"3.9.12.31": [
94516904,
94518240,
94516712,
0,
94518176
],
"3.9.12.37": [
94520808,
94522144,
94522146,
0,
94522080
],
"3.9.12.45": [
94503784,
94505120,
94503592,
0,
94505056
],
"3.9.12.51": [
94555176,
94556512,
94554984,
0,
94556448
]
}

View File

@ -5,15 +5,8 @@
# Author: xaoyaoo
# Date: 2023/10/14
# -------------------------------------------------------------------------------
# from .analyzer.db_parsing import read_img_dat, read_emoji, decompress_CompressContent, read_audio_buf, read_audio, \
# parse_xml_string, read_BytesExtra
# from .ui import app_show_chat, get_user_list, export
from .wx_core import BiasAddr, get_wx_info, get_wx_db, batch_decrypt, decrypt, get_core_db
from .wx_core import merge_db, decrypt_merge, merge_real_time_db, all_merge_real_time_db
from .analyzer import DBPool
from .db import MsgHandler, MicroHandler, \
MediaHandler, OpenIMContactHandler, FavoriteHandler, PublicMsgHandler, DBHandler
from .server import start_falsk
__version__ = "3.1.45"
import os, json
try:
@ -24,7 +17,18 @@ except:
WX_OFFS = {}
WX_OFFS_PATH = None
from .wx_core import BiasAddr, get_wx_info, get_wx_db, batch_decrypt, decrypt, get_core_db
from .wx_core import merge_db, decrypt_merge, merge_real_time_db, all_merge_real_time_db
from .db import DBHandler, MsgHandler, MicroHandler, MediaHandler, OpenIMContactHandler, FavoriteHandler, \
PublicMsgHandler
from .api import start_server, gen_fastapi_app
from .api.export import export_html, export_csv, export_json
# PYWXDUMP_ROOT_PATH = os.path.dirname(__file__)
# db_init = DBPool("DBPOOL_INIT")
__version__ = "3.1.13"
__all__ = ["BiasAddr", "get_wx_info", "get_wx_db", "batch_decrypt", "decrypt", "get_core_db",
"merge_db", "decrypt_merge", "merge_real_time_db", "all_merge_real_time_db",
"DBHandler", "MsgHandler", "MicroHandler", "MediaHandler", "OpenIMContactHandler", "FavoriteHandler",
"PublicMsgHandler", "start_server", "WX_OFFS", "WX_OFFS_PATH", "__version__"]

View File

@ -8,7 +8,6 @@
import sqlite3
import time
from collections import Counter
import pandas as pd
from pywxdump.db.utils import xml2dict
from pywxdump.db import dbMSG
@ -19,6 +18,7 @@ def date_chat_count(chat_data, interval="W"):
:param chat_data: 聊天数据 json {"CreateTime":时间,"Type":消息类型,"SubType":消息子类型,"StrContent":消息内容,"StrTalker":聊天对象,"IsSender":是否发送者}
:param interval: 时间间隔 可选值daymonthyearweek
"""
import pandas as pd
chat_data = pd.DataFrame(chat_data)
chat_data["CreateTime"] = pd.to_datetime(chat_data["CreateTime"])
chat_data["AdjustedTime"] = pd.to_datetime(chat_data["CreateTime"]) - pd.Timedelta(hours=4)

View File

@ -5,9 +5,213 @@
# Author: xaoyaoo
# Date: 2023/12/14
# -------------------------------------------------------------------------------
import os
import subprocess
import sys
import time
import uvicorn
import mimetypes
import logging
from logging.handlers import RotatingFileHandler
from uvicorn.config import LOGGING_CONFIG
from fastapi import FastAPI, Request, Path, Query
from fastapi.staticfiles import StaticFiles
from fastapi.exceptions import RequestValidationError
from starlette.middleware.cors import CORSMiddleware
from starlette.responses import RedirectResponse, FileResponse
from .utils import gc, is_port_in_use, server_loger
from .rjson import ReJson
from .remote_server import rs_api
from .local_server import ls_api
from .utils import get_conf, set_conf
if __name__ == '__main__':
pass
from pywxdump import __version__
def gen_fastapi_app(handler, origins=None):
app = FastAPI(title="wxdump", description="微信工具", version=__version__,
terms_of_service="https://github.com/xaoyaoo/pywxdump",
contact={"name": "xaoyaoo", "url": "https://github.com/xaoyaoo/pywxdump"},
license_info={"name": "MIT License",
"url": "https://github.com/xaoyaoo/PyWxDump/blob/master/LICENSE"})
web_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "ui", "web") # web文件夹路径
# 跨域
if not origins:
origins = [
"http://localhost:5000",
"http://127.0.0.1:5000",
"http://localhost:8080", # 开发环境的客户端地址"
# "http://0.0.0.0:5000",
# "*"
]
app.add_middleware(
CORSMiddleware,
allow_origins=origins, # 允许所有源
allow_credentials=True,
allow_methods=["*"], # 允许所有方法
allow_headers=["*"], # 允许所有头
)
@app.on_event("startup")
async def startup_event():
logger = logging.getLogger("uvicorn")
logger.addHandler(handler)
# 错误处理
@app.exception_handler(RequestValidationError)
async def request_validation_exception_handler(request: Request, exc: RequestValidationError):
# print(request.body)
return ReJson(1002, {"detail": exc.errors()})
# 首页
@app.get("/")
@app.get("/index.html")
async def index():
response = RedirectResponse(url="/s/index.html", status_code=307)
return response
# 路由挂载
app.include_router(rs_api, prefix='/api/rs', tags=['远程api'])
app.include_router(ls_api, prefix='/api/ls', tags=['本地api'])
# 根据文件类型设置mime_type返回文件
@app.get("/s/{filename:path}")
async def serve_file(filename: str):
# 构建完整的文件路径
file_path = os.path.join(web_path, filename)
file_path = os.path.abspath(file_path)
# 检查文件是否存在
if os.path.isfile(file_path):
# 获取文件 MIME 类型
mime_type, _ = mimetypes.guess_type(file_path)
# 如果 MIME 类型为空,则默认为 application/octet-stream
if mime_type is None:
mime_type = "application/octet-stream"
server_loger.warning(f"[+] 无法获取文件 MIME 类型,使用默认值:{mime_type}")
if file_path.endswith(".js"):
mime_type = "text/javascript"
server_loger.info(f"[+] 文件 {file_path} MIME 类型:{mime_type}")
# 返回文件
return FileResponse(file_path, media_type=mime_type)
# 如果文件不存在,返回 404
return {"detail": "Not Found"}, 404
# 静态文件挂载
# if os.path.exists(os.path.join(web_path, "index.html")):
# app.mount("/s", StaticFiles(directory=web_path), name="static")
return app
def start_server(port=5000, online=False, debug=False, isopenBrowser=True,
merge_path="", wx_path="", my_wxid="", ):
"""
启动flask
:param port: 端口号
:param online: 是否在线查看(局域网查看)
:param debug: 是否开启debug模式
:param isopenBrowser: 是否自动打开浏览器
:return:
"""
work_path = os.path.join(os.getcwd(), "wxdump_work") # 临时文件夹,用于存放图片等 # 全局变量
if not os.path.exists(work_path):
os.makedirs(work_path, exist_ok=True)
server_loger.info(f"[+] 创建临时文件夹:{work_path}")
print(f"[+] 创建临时文件夹:{work_path}")
# 日志处理,写入到文件
log_format = '[{levelname[0]}] {asctime} [{name}:{levelno}] {pathname}:{lineno} {message}'
log_datefmt = '%Y-%m-%d %H:%M:%S'
log_file_path = os.path.join(work_path, "wxdump.log")
file_handler = RotatingFileHandler(log_file_path, mode="a", maxBytes=10 * 1024 * 1024, backupCount=3)
formatter = logging.Formatter(fmt=log_format, datefmt=log_datefmt, style='{')
file_handler.setFormatter(formatter)
wx_core_logger = logging.getLogger("wx_core")
db_prepare = logging.getLogger("db_prepare")
# 这几个日志处理器为本项目的日志处理器
server_loger.addHandler(file_handler)
wx_core_logger.addHandler(file_handler)
db_prepare.addHandler(file_handler)
conf_file = os.path.join(work_path, "conf_auto.json") # 用于存放各种基础信息
auto_setting = "auto_setting"
env_file = os.path.join(work_path, ".env") # 用于存放环境变量
# set 环境变量
os.environ["PYWXDUMP_WORK_PATH"] = work_path
os.environ["PYWXDUMP_CONF_FILE"] = conf_file
os.environ["PYWXDUMP_AUTO_SETTING"] = auto_setting
with open(env_file, "w", encoding="utf-8") as f:
f.write(f"PYWXDUMP_WORK_PATH = '{work_path}'\n")
f.write(f"PYWXDUMP_CONF_FILE = '{conf_file}'\n")
f.write(f"PYWXDUMP_AUTO_SETTING = '{auto_setting}'\n")
if merge_path and os.path.exists(merge_path):
my_wxid = my_wxid if my_wxid else "wxid_dbshow"
gc.set_conf(my_wxid, "wxid", my_wxid) # 初始化wxid
gc.set_conf(my_wxid, "merge_path", merge_path) # 初始化merge_path
gc.set_conf(my_wxid, "wx_path", wx_path) # 初始化wx_path
db_config = {"key": my_wxid, "type": "sqlite", "path": merge_path}
gc.set_conf(my_wxid, "db_config", db_config) # 初始化db_config
gc.set_conf(auto_setting, "last", my_wxid) # 初始化last
# 检查端口是否被占用
if online:
host = '0.0.0.0'
else:
host = "127.0.0.1"
if is_port_in_use(host, port):
server_loger.error(f"Port {port} is already in use. Choose a different port.")
print(f"Port {port} is already in use. Choose a different port.")
input("Press Enter to exit...")
return # 退出程序
if isopenBrowser:
try:
# 自动打开浏览器
url = f"http://127.0.0.1:{port}/"
# 根据操作系统使用不同的命令打开默认浏览器
if sys.platform.startswith('darwin'): # macOS
subprocess.call(['open', url])
elif sys.platform.startswith('win'): # Windows
subprocess.call(['start', url], shell=True)
elif sys.platform.startswith('linux'): # Linux
subprocess.call(['xdg-open', url])
else:
server_loger.error(f"Unsupported platform, can't open browser automatically.", exc_info=True)
print("Unsupported platform, can't open browser automatically.")
except Exception as e:
server_loger.error(f"自动打开浏览器失败:{e}", exc_info=True)
time.sleep(1)
server_loger.info(f"启动flask服务host:port{host}:{port}")
print(f"[+] 请使用浏览器访问 http://127.0.0.1:{port}/ 查看聊天记录")
global app
print(f"[+] 如需查看api文档请访问 http://127.0.0.1:{port}/docs ")
origins = [
f"http://localhost:{port}",
f"http://{host}:{port}",
f"http://localhost:8080", # 开发环境的客户端地址"
# f"http://0.0.0.0:{port}",
# "*"
]
app = gen_fastapi_app(file_handler, origins)
LOGGING_CONFIG["formatters"]["default"]["fmt"] = "[%(asctime)s] %(levelprefix)s %(message)s"
LOGGING_CONFIG["formatters"]["access"][
"fmt"] = '[%(asctime)s] %(levelprefix)s %(client_addr)s - "%(request_line)s" %(status_code)s'
config = uvicorn.Config(app=app, host=host, port=port, reload=debug, log_level="info", workers=1, env_file=env_file)
server = uvicorn.Server(config)
server.run()
# uvicorn.run(app=app, host=host, port=port, reload=debug, log_level="info", workers=1, env_file=env_file)
app = None
__all__ = ["start_server", "gen_fastapi_app"]

View File

@ -5,7 +5,6 @@
# Author: xaoyaoo
# Date: 2024/04/20
# -------------------------------------------------------------------------------
if __name__ == '__main__':
pass
from .exportCSV import export_csv
from .exportJSON import export_json
from .exportHtml import export_html

View File

@ -8,18 +8,18 @@
import csv
import json
import os
from ..dbMSG import MsgHandler
from pywxdump.db import DBHandler
def export_csv(wxid, outpath, db_config, page_size=5000):
def export_csv(wxid, outpath, db_config, my_wxid="", page_size=5000):
if not os.path.exists(outpath):
outpath = os.path.join(os.getcwd(), "export" + os.sep + wxid)
if not os.path.exists(outpath):
os.makedirs(outpath)
pmsg = MsgHandler(db_config)
db = DBHandler(db_config, my_wxid)
count = pmsg.get_msg_count(wxid)
count = db.get_msgs_count(wxid)
chatCount = count.get(wxid, 0)
if chatCount == 0:
return False, "没有聊天记录"
@ -27,9 +27,12 @@ def export_csv(wxid, outpath, db_config, page_size=5000):
if page_size > chatCount:
page_size = chatCount + 1
users = {}
for i in range(0, chatCount, page_size):
start_index = i
data, wxid_list = pmsg.get_msg_list(wxid, start_index, page_size)
data, users_t = db.get_msgs(wxid, start_index, page_size)
print(users, users_t)
users.update(users_t)
if len(data) == 0:
return False, "没有聊天记录"
@ -52,7 +55,8 @@ def export_csv(wxid, outpath, db_config, page_size=5000):
src = row.get("src", "")
CreateTime = row.get("CreateTime", "")
csv_writer.writerow([id, MsgSvrID, type_name, is_sender, talker, room_name, msg, src, CreateTime])
with open(os.path.join(outpath, "users.json"), "w", encoding="utf-8") as f:
json.dump(users, f, ensure_ascii=False, indent=4)
return True, f"导出成功: {outpath}"

View File

@ -0,0 +1,46 @@
# -*- coding: utf-8 -*-#
# -------------------------------------------------------------------------------
# Name: exportCSV.py
# Description:
# Author: xaoyaoo
# Date: 2024/04/20
# -------------------------------------------------------------------------------
import json
import os
from pywxdump.db import DBHandler
def export_html(wxid, outpath, db_config, my_wxid=""):
if not os.path.exists(outpath):
outpath = os.path.join(os.getcwd(), "export" + os.sep + wxid)
if not os.path.exists(outpath):
os.makedirs(outpath)
db = DBHandler(db_config, my_wxid)
count = db.get_msgs_count(wxid)
chatCount = count.get(wxid, 0)
if chatCount == 0:
return False, "没有聊天记录"
msgs, users = db.get_msgs(wxid, 0, chatCount + 1)
if len(msgs) == 0:
return False, "没有聊天记录"
data_js = (
"localStorage.setItem('isUseLocalData', 't') // 't' : 'f' \n"
f"const local_msg_count = {chatCount}\n"
f"const local_mywxid = '{my_wxid}' \n"
f"const local_user_list = {json.dumps(users, ensure_ascii=False, indent=None )} \n"
f"const local_msg_list = {json.dumps(msgs, ensure_ascii=False, indent=None )} \n"
)
save_path = os.path.join(outpath, f"data.js")
with open(save_path, "w", encoding="utf-8") as f:
f.write(data_js)
return True, f"导出成功: {outpath}"
if __name__ == '__main__':
pass

View File

@ -7,31 +7,35 @@
# -------------------------------------------------------------------------------
import json
import os
from ..dbMSG import MsgHandler
from pywxdump.db import DBHandler
def export_json(wxid, outpath, db_config):
def export_json(wxid, outpath, db_config, my_wxid="", indent=4):
if not os.path.exists(outpath):
outpath = os.path.join(os.getcwd(), "export" + os.sep + wxid)
if not os.path.exists(outpath):
os.makedirs(outpath)
pmsg = MsgHandler(db_config)
db = DBHandler(db_config, my_wxid)
count = pmsg.get_msg_count(wxid)
count = db.get_msgs_count(wxid)
chatCount = count.get(wxid, 0)
if chatCount == 0:
return False, "没有聊天记录"
users = {}
page_size = chatCount + 1
for i in range(0, chatCount, page_size):
start_index = i
data, wxid_list = pmsg.get_msg_list(wxid, start_index, page_size)
data, users_t = db.get_msgs(wxid, start_index, page_size)
users.update(users_t)
if len(data) == 0:
return False, "没有聊天记录"
save_path = os.path.join(outpath, f"{wxid}_{i}_{i + page_size}.json")
with open(save_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=4)
json.dump(data, f, ensure_ascii=False, indent=indent)
with open(os.path.join(outpath, "users.json"), "w", encoding="utf-8") as f:
json.dump(users, f, ensure_ascii=False, indent=indent)
return True, f"导出成功: {outpath}"

View File

@ -5,61 +5,56 @@
# Author: xaoyaoo
# Date: 2024/08/01
# -------------------------------------------------------------------------------
import base64
import json
import logging
import os
import re
import time
import shutil
import pythoncom
import pywxdump
from flask import Flask, request, render_template, g, Blueprint, send_file, make_response, session
from pywxdump import get_core_db, all_merge_real_time_db, get_wx_db
from pywxdump.api.rjson import ReJson, RqJson
from pywxdump.api.utils import get_conf, get_conf_wxids, set_conf, error9999, gen_base64, validate_title, \
get_conf_local_wxid, ls_loger, random_str
from pywxdump import get_wx_info, WX_OFFS, batch_decrypt, BiasAddr, merge_db, decrypt_merge, merge_real_time_db
from pydantic import BaseModel
from fastapi import APIRouter, Body
from pywxdump.db import DBHandler, download_file, export_csv, export_json
from pywxdump import all_merge_real_time_db, get_wx_db
from pywxdump import get_wx_info, batch_decrypt, BiasAddr, merge_db, decrypt_merge
ls_api = Blueprint('ls_api', __name__, template_folder='../ui/web', static_folder='../ui/web/assets/', )
ls_api.debug = False
from .rjson import ReJson, RqJson
from .utils import error9999, ls_loger, random_str, gc
ls_api = APIRouter()
# 以下为初始化相关 *******************************************************************************************************
@ls_api.route('/api/ls/init_last_local_wxid', methods=["GET", 'POST'])
@ls_api.post('/init_last_local_wxid')
@error9999
def init_last_local_wxid():
"""
初始化包括key
:return:
"""
local_wxid = get_conf_local_wxid(g.caf)
local_wxid.remove(g.at)
local_wxid = gc.get_local_wxids()
local_wxid.remove(gc.at)
if local_wxid:
return ReJson(0, {"local_wxids": local_wxid})
return ReJson(0, {"local_wxids": []})
@ls_api.route('/api/ls/init_last', methods=["GET", 'POST'])
@ls_api.post('/init_last')
@error9999
def init_last():
def init_last(my_wxid: str = Body(..., embed=True)):
"""
是否初始化
:return:
"""
my_wxid = request.json.get("my_wxid", "")
my_wxid = my_wxid.strip().strip("'").strip('"') if isinstance(my_wxid, str) else ""
ls_loger.info(f"[+] init_last: {my_wxid}")
if not my_wxid:
my_wxid = get_conf(g.caf, "auto_setting", "last")
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
if my_wxid:
set_conf(g.caf, "auto_setting", "last", my_wxid)
merge_path = get_conf(g.caf, my_wxid, "merge_path")
wx_path = get_conf(g.caf, my_wxid, "wx_path")
key = get_conf(g.caf, my_wxid, "key")
gc.set_conf(gc.at, "last", my_wxid)
merge_path = gc.get_conf(my_wxid, "merge_path")
wx_path = gc.get_conf(my_wxid, "wx_path")
key = gc.get_conf(my_wxid, "key")
rdata = {
"merge_path": merge_path,
"wx_path": wx_path,
@ -72,16 +67,23 @@ def init_last():
return ReJson(0, {"is_init": False, "my_wxid": ""})
@ls_api.route('/api/ls/init_key', methods=["GET", 'POST'])
class InitKeyRequest(BaseModel):
wx_path: str
key: str
my_wxid: str
@ls_api.post('/init_key')
@error9999
def init_key():
def init_key(request: InitKeyRequest):
"""
初始化包括key
初始化key
:param request:
:return:
"""
wx_path = request.json.get("wx_path", "").strip().strip("'").strip('"')
key = request.json.get("key", "").strip().strip("'").strip('"')
my_wxid = request.json.get("my_wxid", "").strip().strip("'").strip('"')
wx_path = request.wx_path.strip().strip("'").strip('"')
key = request.key.strip().strip("'").strip('"')
my_wxid = request.my_wxid.strip().strip("'").strip('"')
if not wx_path:
return ReJson(1002, body=f"wx_path is required: {wx_path}")
if not os.path.exists(wx_path):
@ -96,7 +98,7 @@ def init_key():
# pmsg = DBHandler(db_config)
# # pmsg.close_all_connection()
out_path = os.path.join(g.work_path, "decrypted", my_wxid) if my_wxid else os.path.join(g.work_path, "decrypted")
out_path = os.path.join(gc.work_path, "decrypted", my_wxid) if my_wxid else os.path.join(gc.work_path, "decrypted")
# 检查文件夹中文件是否被占用
if os.path.exists(out_path):
try:
@ -110,9 +112,9 @@ def init_key():
time.sleep(1)
if code:
# 移动merge_save_path到g.work_path/my_wxid
if not os.path.exists(os.path.join(g.work_path, my_wxid)):
os.makedirs(os.path.join(g.work_path, my_wxid))
merge_save_path_new = os.path.join(g.work_path, my_wxid, "merge_all.db")
if not os.path.exists(os.path.join(gc.work_path, my_wxid)):
os.makedirs(os.path.join(gc.work_path, my_wxid))
merge_save_path_new = os.path.join(gc.work_path, my_wxid, "merge_all.db")
shutil.move(merge_save_path, str(merge_save_path_new))
# 删除out_path
@ -127,12 +129,13 @@ def init_key():
"type": "sqlite",
"path": merge_save_path_new
}
set_conf(g.caf, my_wxid, "db_config", db_config)
set_conf(g.caf, my_wxid, "merge_path", merge_save_path_new)
set_conf(g.caf, my_wxid, "wx_path", wx_path)
set_conf(g.caf, my_wxid, "key", key)
set_conf(g.caf, my_wxid, "my_wxid", my_wxid)
set_conf(g.caf, "auto_setting", "last", my_wxid)
gc.set_conf(my_wxid, "db_config", db_config)
gc.set_conf(my_wxid, "db_config", db_config)
gc.set_conf(my_wxid, "merge_path", merge_save_path_new)
gc.set_conf(my_wxid, "wx_path", wx_path)
gc.set_conf(my_wxid, "key", key)
gc.set_conf(my_wxid, "my_wxid", my_wxid)
gc.set_conf(gc.at, "last", my_wxid)
rdata = {
"merge_path": merge_save_path_new,
"wx_path": wx_path,
@ -145,16 +148,22 @@ def init_key():
return ReJson(2001, body=merge_save_path)
@ls_api.route('/api/ls/init_nokey', methods=["GET", 'POST'])
class InitNoKeyRequest(BaseModel):
merge_path: str
wx_path: str
my_wxid: str
@ls_api.post('/init_nokey')
@error9999
def init_nokey():
def init_nokey(request: InitNoKeyRequest):
"""
初始化包括key
:return:
"""
merge_path = request.json.get("merge_path", "").strip().strip("'").strip('"')
wx_path = request.json.get("wx_path", "").strip().strip("'").strip('"')
my_wxid = request.json.get("my_wxid", "").strip().strip("'").strip('"')
merge_path = request.merge_path.strip().strip("'").strip('"')
wx_path = request.wx_path.strip().strip("'").strip('"')
my_wxid = request.my_wxid.strip().strip("'").strip('"')
if not wx_path:
return ReJson(1002, body=f"wx_path is required: {wx_path}")
@ -165,18 +174,18 @@ def init_nokey():
if not my_wxid:
return ReJson(1002, body=f"my_wxid is required: {my_wxid}")
key = get_conf(g.caf, my_wxid, "key")
key = gc.get_conf(my_wxid, "key")
db_config = {
"key": random_str(16),
"type": "sqlite",
"path": merge_path
}
set_conf(g.caf, my_wxid, "db_config", db_config)
set_conf(g.caf, my_wxid, "merge_path", merge_path)
set_conf(g.caf, my_wxid, "wx_path", wx_path)
set_conf(g.caf, my_wxid, "key", key)
set_conf(g.caf, my_wxid, "my_wxid", my_wxid)
set_conf(g.caf, g.at, "last", my_wxid)
gc.set_conf(my_wxid, "db_config", db_config)
gc.set_conf(my_wxid, "merge_path", merge_path)
gc.set_conf(my_wxid, "wx_path", wx_path)
gc.set_conf(my_wxid, "key", key)
gc.set_conf(my_wxid, "my_wxid", my_wxid)
gc.set_conf(gc.at, "last", my_wxid)
rdata = {
"merge_path": merge_path,
"wx_path": wx_path,
@ -190,24 +199,25 @@ def init_nokey():
# END 以上为初始化相关 ***************************************************************************************************
@ls_api.route('/api/ls/realtimemsg', methods=["GET", "POST"])
@ls_api.api_route('/realtimemsg', methods=["GET", "POST"])
@error9999
def get_real_time_msg():
"""
获取实时消息 使用 merge_real_time_db()函数
:return:
"""
my_wxid = get_conf(g.caf, g.at, "last")
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
merge_path = get_conf(g.caf, my_wxid, "merge_path")
key = get_conf(g.caf, my_wxid, "key")
wx_path = get_conf(g.caf, my_wxid, "wx_path")
if not merge_path or not key or not wx_path or not wx_path:
merge_path = gc.get_conf(my_wxid, "merge_path")
key = gc.get_conf(my_wxid, "key")
wx_path = gc.get_conf(my_wxid, "wx_path")
if not merge_path or not key or not wx_path:
return ReJson(1002, body="msg_path or media_path or wx_path or key is required")
code, ret = all_merge_real_time_db(key=key, wx_path=wx_path, merge_path=merge_path)
real_time_exe_path = gc.get_conf(gc.at, "real_time_exe_path")
code, ret = all_merge_real_time_db(key=key, wx_path=wx_path, merge_path=merge_path,
real_time_exe_path=real_time_exe_path)
if code:
return ReJson(0, ret)
else:
@ -216,7 +226,7 @@ def get_real_time_msg():
# start 这部分为专业工具的api *********************************************************************************************
@ls_api.route('/api/ls/wxinfo', methods=["GET", 'POST'])
@ls_api.api_route('/wxinfo', methods=["GET", 'POST'])
@error9999
def get_wxinfo():
"""
@ -224,24 +234,33 @@ def get_wxinfo():
:return:
"""
import pythoncom
pythoncom.CoInitialize()
from pywxdump import WX_OFFS
pythoncom.CoInitialize() # 初始化COM库
wxinfos = get_wx_info(WX_OFFS)
pythoncom.CoUninitialize()
pythoncom.CoUninitialize() # 释放COM库
return ReJson(0, wxinfos)
@ls_api.route('/api/ls/biasaddr', methods=["GET", 'POST'])
class BiasAddrRequest(BaseModel):
mobile: str
name: str
account: str
key: str = ""
wxdbPath: str = ""
@ls_api.post('/biasaddr')
@error9999
def biasaddr():
def get_biasaddr(request: BiasAddrRequest):
"""
BiasAddr
:return:
"""
mobile = request.json.get("mobile")
name = request.json.get("name")
account = request.json.get("account")
key = request.json.get("key", "")
wxdbPath = request.json.get("wxdbPath", "")
mobile = request.mobile
name = request.name
account = request.account
key = request.key
wxdbPath = request.wxdbPath
if not mobile or not name or not account:
return ReJson(1002)
pythoncom.CoInitialize()
@ -249,39 +268,33 @@ def biasaddr():
return ReJson(0, str(rdata))
@ls_api.route('/api/ls/decrypt', methods=["GET", 'POST'])
@ls_api.api_route('/decrypt', methods=["GET", 'POST'])
@error9999
def decrypt():
def get_decrypt(key: str, wxdbPath: str, outPath: str = ""):
"""
解密
:return:
"""
key = request.json.get("key")
if not key:
return ReJson(1002)
wxdb_path = request.json.get("wxdbPath")
if not wxdb_path:
return ReJson(1002)
out_path = request.json.get("outPath")
if not out_path:
out_path = g.tmp_path
wxinfos = batch_decrypt(key, wxdb_path, out_path=out_path)
if not outPath:
outPath = gc.work_path
wxinfos = batch_decrypt(key, wxdbPath, out_path=outPath)
return ReJson(0, str(wxinfos))
@ls_api.route('/api/ls/merge', methods=["GET", 'POST'])
class MergeRequest(BaseModel):
dbPath: str
outPath: str
@ls_api.post('/merge')
@error9999
def merge():
def get_merge(request: MergeRequest):
"""
合并
:return:
"""
wxdb_path = request.json.get("dbPath")
if not wxdb_path:
return ReJson(1002)
out_path = request.json.get("outPath")
if not out_path:
return ReJson(1002)
wxdb_path = request.dbPath
out_path = request.outPath
db_path = get_wx_db(wxdb_path)
# for i in db_path:print(i)
rdata = merge_db(db_path, out_path)

View File

@ -1,44 +1,43 @@
# -*- coding: utf-8 -*-#
# -------------------------------------------------------------------------------
# Name: chat_api.py
# Name: remote_server.py
# Description:
# Author: xaoyaoo
# Date: 2024/01/02
# -------------------------------------------------------------------------------
import base64
import json
import logging
import os
import re
import time
import shutil
import pythoncom
from collections import Counter
from urllib.parse import quote, unquote
from typing import List, Optional
from pydantic import BaseModel
from fastapi import APIRouter, Response, Body, Query, Request
from starlette.responses import StreamingResponse, FileResponse
import pywxdump
from pywxdump import decrypt_merge, get_core_db
from pywxdump.db import DBHandler
from pywxdump.db.utils import download_file, dat2img
from flask import Flask, request, render_template, g, Blueprint, send_file, make_response, session
from pywxdump import get_core_db, all_merge_real_time_db
from pywxdump.api.rjson import ReJson, RqJson
from pywxdump.api.utils import get_conf, get_conf_wxids, set_conf, error9999, gen_base64, validate_title, \
get_conf_local_wxid
from pywxdump import get_wx_info, WX_OFFS, batch_decrypt, BiasAddr, merge_db, decrypt_merge, merge_real_time_db
from .export import export_csv, export_json, export_html
from .rjson import ReJson, RqJson
from .utils import error9999, gc, asyncError9999, rs_loger
from pywxdump.db import DBHandler, download_file, export_csv, export_json, dat2img
# app = Flask(__name__, static_folder='../ui/web/dist', static_url_path='/')
rs_api = Blueprint('rs_api', __name__, template_folder='../ui/web', static_folder='../ui/web/assets/', )
rs_api.debug = False
rs_api = APIRouter()
# 是否初始化
@rs_api.route('/api/rs/is_init', methods=["GET", 'POST'])
@rs_api.api_route('/is_init', methods=["GET", 'POST'])
@error9999
def is_init():
"""
是否初始化
:return:
"""
local_wxids = get_conf_local_wxid(g.caf)
local_wxids = gc.get_local_wxids()
if len(local_wxids) > 1:
return ReJson(0, True)
return ReJson(0, False)
@ -46,74 +45,64 @@ def is_init():
# start 以下为聊天联系人相关api *******************************************************************************************
@rs_api.route('/api/rs/mywxid', methods=["GET", 'POST'])
@rs_api.api_route('/mywxid', methods=["GET", 'POST'])
@error9999
def mywxid():
"""
获取我的微信id
:return:
"""
my_wxid = get_conf(g.caf, g.at, "last")
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
return ReJson(0, {"my_wxid": my_wxid})
@rs_api.route('/api/rs/user_session_list', methods=["GET", 'POST'])
@rs_api.api_route('/user_session_list', methods=["GET", 'POST'])
@error9999
def user_session_list():
"""
获取联系人列表
:return:
"""
my_wxid = get_conf(g.caf, g.at, "last")
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
db_config = get_conf(g.caf, my_wxid, "db_config")
db = DBHandler(db_config)
db_config = gc.get_conf(my_wxid, "db_config")
db = DBHandler(db_config, my_wxid=my_wxid)
ret = db.get_session_list()
return ReJson(0, list(ret.values()))
@rs_api.route('/api/rs/user_labels_dict', methods=["GET", 'POST'])
@rs_api.api_route('/user_labels_dict', methods=["GET", 'POST'])
@error9999
def user_labels_dict():
"""
获取标签字典
:return:
"""
my_wxid = get_conf(g.caf, g.at, "last")
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
db_config = get_conf(g.caf, my_wxid, "db_config")
db = DBHandler(db_config)
db_config = gc.get_conf(my_wxid, "db_config")
db = DBHandler(db_config, my_wxid=my_wxid)
user_labels_dict = db.get_labels()
return ReJson(0, user_labels_dict)
@rs_api.route('/api/rs/user_list', methods=["GET", 'POST'])
@rs_api.post('/user_list')
@error9999
def user_list():
def user_list(word: str = Body("", embed=True), wxids: List[str] = Body(None), labels: List[str] = Body(None)):
"""
获取联系人列表可用于搜索
:return:
"""
if request.method == "GET":
word = request.args.get("word", "")
wxids = request.args.get("wxids", [])
labels = request.args.get("labels", [])
elif request.method == "POST":
word = request.json.get("word", "")
wxids = request.json.get("wxids", [])
labels = request.json.get("labels", [])
else:
return ReJson(1003, msg="Unsupported method")
if isinstance(wxids, str) and wxids == '' or wxids is None: wxids = []
if isinstance(labels, str) and labels == '' or labels is None: labels = []
my_wxid = get_conf(g.caf, g.at, "last")
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
db_config = get_conf(g.caf, my_wxid, "db_config")
db = DBHandler(db_config)
users = db.get_user(word, wxids, labels)
db_config = gc.get_conf(my_wxid, "db_config")
db = DBHandler(db_config, my_wxid=my_wxid)
users = db.get_user(word=word, wxids=wxids, labels=labels)
return ReJson(0, users)
@ -121,26 +110,58 @@ def user_list():
# start 以下为聊天记录相关api *********************************************************************************************
@rs_api.route('/api/rs/imgsrc/<path:imgsrc>', methods=["GET", 'POST'])
@rs_api.post('/msg_count')
@error9999
def get_imgsrc(imgsrc):
def msg_count(wxids: Optional[List[str]] = Body(..., embed=True)):
"""
获取联系人的聊天记录数量
:return:
"""
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
db_config = gc.get_db_config()
db = DBHandler(db_config, my_wxid=my_wxid)
count = db.get_msgs_count(wxids)
return ReJson(0, count)
@rs_api.api_route('/msg_list', methods=["GET", 'POST'])
@error9999
def get_msgs(wxid: str = Body(...), start: int = Body(...), limit: int = Body(...)):
"""
获取联系人的聊天记录
:return:
"""
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
db_config = gc.get_conf(my_wxid, "db_config")
db = DBHandler(db_config, my_wxid=my_wxid)
msgs, users = db.get_msgs(wxids=wxid, start_index=start, page_size=limit)
return ReJson(0, {"msg_list": msgs, "user_list": users})
@rs_api.get('/imgsrc')
@asyncError9999
async def get_imgsrc(request: Request):
"""
获取图片,
1. 从网络获取图片主要功能只是下载图片缓存到本地
2. 读取本地图片
:return:
"""
imgsrc = unquote(str(request.query_params).replace("src=", "", 1))
if not imgsrc:
return ReJson(1002)
if imgsrc.startswith("FileStorage"): # 如果是本地图片文件则调用get_img
my_wxid = get_conf(g.caf, g.at, "last")
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
wx_path = get_conf(g.caf, my_wxid, "wx_path")
wx_path = gc.get_conf(my_wxid, "wx_path")
img_path = imgsrc.replace("\\\\", "\\")
img_tmp_path = os.path.join(g.work_path, my_wxid, "img")
img_tmp_path = os.path.join(gc.work_path, my_wxid, "img")
original_img_path = os.path.join(wx_path, img_path)
if os.path.exists(original_img_path):
rc, fomt, md5, out_bytes = dat2img(original_img_path)
@ -148,21 +169,21 @@ def get_imgsrc(imgsrc):
return ReJson(1001, body=original_img_path)
imgsavepath = os.path.join(str(img_tmp_path), img_path + "_" + "".join([md5, fomt]))
if os.path.exists(imgsavepath):
return send_file(imgsavepath)
return FileResponse(imgsavepath)
if not os.path.exists(os.path.dirname(imgsavepath)):
os.makedirs(os.path.dirname(imgsavepath))
with open(imgsavepath, "wb") as f:
f.write(out_bytes)
return send_file(imgsavepath)
return Response(content=out_bytes, media_type="image/jpeg")
else:
return ReJson(1001, body=f"{original_img_path} not exists")
elif imgsrc.startswith("http://") or imgsrc.startswith("https://"):
# 将?后面的参数连接到imgsrc
imgsrc = imgsrc + "?" + request.query_string.decode("utf-8") if request.query_string else imgsrc
my_wxid = get_conf(g.caf, g.at, "last")
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
img_tmp_path = os.path.join(g.work_path, my_wxid, "imgsrc")
img_tmp_path = os.path.join(gc.work_path, my_wxid, "imgsrc")
if not os.path.exists(img_tmp_path):
os.makedirs(img_tmp_path)
file_name = imgsrc.replace("http://", "").replace("https://", "").replace("/", "_").replace("?", "_")
@ -173,105 +194,69 @@ def get_imgsrc(imgsrc):
img_path_all = os.path.join(str(img_tmp_path), file_name)
if os.path.exists(img_path_all):
return send_file(img_path_all)
return FileResponse(img_path_all)
else:
download_file(imgsrc, img_path_all)
# proxies = {
# "http": "http://127.0.0.1:10809",
# "https": "http://127.0.0.1:10809",
# }
proxies = None
download_file(imgsrc, img_path_all, proxies=proxies)
if os.path.exists(img_path_all):
return send_file(img_path_all)
return FileResponse(img_path_all)
else:
return ReJson(4004, body=imgsrc)
else:
return ReJson(1002, body=imgsrc)
@rs_api.route('/api/rs/msg_count', methods=["GET", 'POST'])
@error9999
def msg_count():
@rs_api.api_route('/video', methods=["GET", 'POST'])
def get_video(request: Request):
"""
获取联系人的聊天记录数量
获取视频
:return:
"""
if request.method == "GET":
wxid = request.args.get("wxids", [])
elif request.method == "POST":
wxid = request.json.get("wxids", [])
else:
return ReJson(1003, msg="Unsupported method")
my_wxid = get_conf(g.caf, g.at, "last")
videoPath = unquote(str(request.query_params).replace("src=", "", 1))
if not videoPath:
return ReJson(1002)
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
db_config = get_conf(g.caf, my_wxid, "db_config")
db = DBHandler(db_config)
chat_count = db.get_msg_count(wxid)
chat_count1 = db.get_plc_msg_count(wxid) if db.PublicMsg_exist else {}
# 合并两个字典相同key则将value相加
count = {k: chat_count.get(k, 0) + chat_count1.get(k, 0) for k in
list(set(list(chat_count.keys()) + list(chat_count1.keys())))}
return ReJson(0, count)
@rs_api.route('/api/rs/msg_list', methods=["GET", 'POST'])
@error9999
def get_msgs():
my_wxid = get_conf(g.caf, g.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
db_config = get_conf(g.caf, my_wxid, "db_config")
start = request.json.get("start")
limit = request.json.get("limit")
wxid = request.json.get("wxid")
if not wxid:
return ReJson(1002, body=f"wxid is required: {wxid}")
if start and isinstance(start, str) and start.isdigit():
start = int(start)
if limit and isinstance(limit, str) and limit.isdigit():
limit = int(limit)
if start is None or limit is None:
return ReJson(1002, body=f"start or limit is required {start} {limit}")
if not isinstance(start, int) and not isinstance(limit, int):
return ReJson(1002, body=f"start or limit is not int {start} {limit}")
db = DBHandler(db_config)
msgs, wxid_list = db.get_msg_list(wxid=wxid, start_index=start, page_size=limit)
if not msgs and db.PublicMsg_exist:
msgs, wxid_list = db.get_plc_msg_list(wxid=wxid, start_index=start, page_size=limit)
wxid_list.append(my_wxid)
user = db.get_user_list(wxids=wxid_list)
return ReJson(0, {"msg_list": msgs, "user_list": user})
@rs_api.route('/api/rs/video/<path:videoPath>', methods=["GET", 'POST'])
def get_video(videoPath):
my_wxid = get_conf(g.caf, g.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
wx_path = get_conf(g.caf, my_wxid, "wx_path")
wx_path = gc.get_conf(my_wxid, "wx_path")
videoPath = videoPath.replace("\\\\", "\\")
video_tmp_path = os.path.join(g.work_path, my_wxid, "video")
video_tmp_path = os.path.join(gc.work_path, my_wxid, "video")
original_img_path = os.path.join(wx_path, videoPath)
if not os.path.exists(original_img_path):
return ReJson(5002)
# 复制文件到临时文件夹
assert isinstance(video_tmp_path, str)
video_save_path = os.path.join(video_tmp_path, videoPath)
if not os.path.exists(os.path.dirname(video_save_path)):
os.makedirs(os.path.dirname(video_save_path))
if os.path.exists(video_save_path):
return send_file(video_save_path)
return FileResponse(path=video_save_path)
shutil.copy(original_img_path, video_save_path)
return send_file(original_img_path)
return FileResponse(path=video_save_path)
@rs_api.route('/api/rs/audio/<path:savePath>', methods=["GET", 'POST'])
def get_audio(savePath):
my_wxid = get_conf(g.caf, g.at, "last")
@rs_api.api_route('/audio', methods=["GET", 'POST'])
def get_audio(request: Request):
"""
获取语音
:return:
"""
savePath = unquote(str(request.query_params).replace("src=", "", 1)).replace("audio\\", "", 1)
if not savePath:
return ReJson(1002)
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
db_config = get_conf(g.caf, my_wxid, "db_config")
db_config = gc.get_conf(my_wxid, "db_config")
savePath = os.path.join(g.work_path, my_wxid, "audio", savePath) # 这个是从url中获取的
savePath = os.path.join(gc.work_path, my_wxid, "audio", savePath) # 这个是从url中获取的
if os.path.exists(savePath):
return send_file(savePath)
assert isinstance(savePath, str)
return FileResponse(path=savePath, media_type='audio/mpeg')
MsgSvrID = savePath.split("_")[-1].replace(".wav", "")
if not savePath:
@ -281,27 +266,26 @@ def get_audio(savePath):
if not os.path.exists(os.path.dirname(savePath)):
os.makedirs(os.path.dirname(savePath))
db = DBHandler(db_config)
db = DBHandler(db_config, my_wxid=my_wxid)
wave_data = db.get_audio(MsgSvrID, is_play=False, is_wave=True, save_path=savePath, rate=24000)
if not wave_data:
return ReJson(1001, body="wave_data is required")
if os.path.exists(savePath):
return send_file(savePath)
assert isinstance(savePath, str)
return FileResponse(path=savePath, media_type='audio/mpeg')
else:
return ReJson(4004, body=savePath)
@rs_api.route('/api/rs/file_info', methods=["GET", 'POST'])
def get_file_info():
file_path = request.args.get("file_path")
file_path = request.json.get("file_path", file_path)
@rs_api.api_route('/file_info', methods=["GET", 'POST'])
def get_file_info(file_path: str = Body(..., embed=True)):
if not file_path:
return ReJson(1002)
my_wxid = get_conf(g.caf, g.at, "last")
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
wx_path = get_conf(g.caf, my_wxid, "wx_path")
wx_path = gc.get_conf(my_wxid, "wx_path")
all_file_path = os.path.join(wx_path, file_path)
if not os.path.exists(all_file_path):
@ -311,65 +295,101 @@ def get_file_info():
return ReJson(0, {"file_name": file_name, "file_size": str(file_size)})
@rs_api.route('/api/rs/file/<path:filePath>', methods=["GET", 'POST'])
def get_file(filePath):
my_wxid = get_conf(g.caf, g.at, "last")
@rs_api.get('/file')
def get_file(request: Request):
"""
获取文件
:return:
"""
file_path = unquote(str(request.query_params).replace("src=", "", 1))
if not file_path:
return ReJson(1002)
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
wx_path = get_conf(g.caf, my_wxid, "wx_path")
wx_path = gc.get_conf(my_wxid, "wx_path")
all_file_path = os.path.join(wx_path, filePath)
all_file_path = os.path.join(wx_path, file_path)
if not os.path.exists(all_file_path):
return ReJson(5002)
return send_file(all_file_path)
def file_iterator(file_path, chunk_size=8192):
with open(file_path, "rb") as f:
while True:
chunk = f.read(chunk_size)
if not chunk:
break
yield chunk
headers = {
"Content-Disposition": f'attachment; filename*=UTF-8\'\'{quote(os.path.basename(all_file_path))}',
}
return StreamingResponse(file_iterator(all_file_path), media_type="application/octet-stream", headers=headers)
# end 以上为聊天记录相关api *********************************************************************************************
# start 导出聊天记录 *****************************************************************************************************
class ExportEndbRequest(BaseModel):
wx_path: str = ""
outpath: str = ""
key: str = ""
@rs_api.route('/api/rs/export_endb', methods=["GET", 'POST'])
def get_export_endb():
@rs_api.api_route('/export_endb', methods=["GET", 'POST'])
def get_export_endb(request: ExportEndbRequest):
"""
导出加密数据库
:return:
"""
my_wxid = get_conf(g.caf, g.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
wx_path = get_conf(g.caf, my_wxid, "wx_path")
wx_path = request.json.get("wx_path", wx_path)
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
wx_path = request.wx_path
if not wx_path:
wx_path = gc.get_conf(my_wxid, "wx_path")
if not os.path.exists(wx_path if wx_path else ""):
return ReJson(1002, body=f"wx_path is required: {wx_path}")
if not os.path.exists(wx_path):
return ReJson(1001, body=f"wx_path not exists: {wx_path}")
# 分割wx_path的文件名和父目录
code, wxdbpaths = get_core_db(wx_path)
if not code:
return ReJson(2001, body=wxdbpaths)
outpath = os.path.join(g.work_path, "export", my_wxid, "endb")
outpath = os.path.join(gc.work_path, "export", my_wxid, "endb")
if not os.path.exists(outpath):
os.makedirs(outpath)
for wxdb in wxdbpaths:
# 复制wxdb->outpath, os.path.basename(wxdb)
assert isinstance(outpath, str) # 为了解决pycharm的警告, 无实际意义
shutil.copy(wxdb, os.path.join(outpath, os.path.basename(wxdb)))
wxdb_path = wxdb.get("db_path")
shutil.copy(wxdb_path, os.path.join(outpath, os.path.basename(wxdb_path)))
return ReJson(0, body=outpath)
@rs_api.route('/api/rs/export_dedb', methods=["GET", "POST"])
def get_export_dedb():
class ExportDedbRequest(BaseModel):
wx_path: str = ""
outpath: str = ""
key: str = ""
@rs_api.api_route('/export_dedb', methods=["GET", "POST"])
def get_export_dedb(request: ExportDedbRequest):
"""
导出解密数据库
:return:
"""
my_wxid = get_conf(g.caf, g.at, "last")
key = request.key
wx_path = request.wx_path
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
key = request.json.get("key", get_conf(g.caf, my_wxid, "key"))
wx_path = request.json.get("wx_path", get_conf(g.caf, my_wxid, "wx_path"))
if not key:
key = gc.get_conf(my_wxid, "key")
if not wx_path:
wx_path = gc.get_conf(my_wxid, "wx_path")
if not key:
return ReJson(1002, body=f"key is required: {key}")
@ -378,10 +398,10 @@ def get_export_dedb():
if not os.path.exists(wx_path):
return ReJson(1001, body=f"wx_path not exists: {wx_path}")
outpath = os.path.join(g.work_path, "export", my_wxid, "dedb")
outpath = os.path.join(gc.work_path, "export", my_wxid, "dedb")
if not os.path.exists(outpath):
os.makedirs(outpath)
assert isinstance(outpath, str)
code, merge_save_path = decrypt_merge(wx_path=wx_path, key=key, outpath=outpath)
time.sleep(1)
if code:
@ -390,17 +410,16 @@ def get_export_dedb():
return ReJson(2001, body=merge_save_path)
@rs_api.route('/api/rs/export_csv', methods=["GET", 'POST'])
def get_export_csv():
@rs_api.api_route('/export_csv', methods=["GET", 'POST'])
def get_export_csv(wxid: str = Body(..., embed=True)):
"""
导出csv
:return:
"""
my_wxid = get_conf(g.caf, g.at, "last")
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
db_config = get_conf(g.caf, my_wxid, "db_config")
db_config = gc.get_conf(my_wxid, "db_config")
wxid = request.json.get("wxid")
# st_ed_time = request.json.get("datetime", [0, 0])
if not wxid:
return ReJson(1002, body=f"username is required: {wxid}")
@ -410,36 +429,71 @@ def get_export_csv():
# if not isinstance(start, int) or not isinstance(end, int) or start >= end:
# return ReJson(1002, body=f"datetime is required: {st_ed_time}")
outpath = os.path.join(g.work_path, "export", my_wxid, "csv", wxid)
outpath = os.path.join(gc.work_path, "export", my_wxid, "csv", wxid)
if not os.path.exists(outpath):
os.makedirs(outpath)
code, ret = export_csv(wxid, outpath, db_config)
code, ret = export_csv(wxid, outpath, db_config, my_wxid=my_wxid)
if code:
return ReJson(0, ret)
else:
return ReJson(2001, body=ret)
@rs_api.route('/api/rs/export_json', methods=["GET", 'POST'])
def get_export_json():
@rs_api.api_route('/export_json', methods=["GET", 'POST'])
def get_export_json(wxid: str = Body(..., embed=True)):
"""
导出json
:return:
"""
my_wxid = get_conf(g.caf, g.at, "last")
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
db_config = get_conf(g.caf, my_wxid, "db_config")
db_config = gc.get_conf(my_wxid, "db_config")
wxid = request.json.get("wxid")
if not wxid:
return ReJson(1002, body=f"username is required: {wxid}")
outpath = os.path.join(g.work_path, "export", my_wxid, "json", wxid)
outpath = os.path.join(gc.work_path, "export", my_wxid, "json", wxid)
if not os.path.exists(outpath):
os.makedirs(outpath)
code, ret = export_json(wxid, outpath, db_config)
code, ret = export_json(wxid, outpath, db_config, my_wxid=my_wxid)
if code:
return ReJson(0, ret)
else:
return ReJson(2001, body=ret)
class ExportHtmlRequest(BaseModel):
wxid: str
@rs_api.api_route('/export_html', methods=["GET", 'POST'])
def get_export_html(wxid: str = Body(..., embed=True)):
"""
导出json
:return:
"""
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
db_config = gc.get_conf(my_wxid, "db_config")
if not wxid:
return ReJson(1002, body=f"username is required: {wxid}")
html_outpath = os.path.join(gc.work_path, "export", my_wxid, "html")
if not os.path.exists(html_outpath):
os.makedirs(html_outpath)
assert isinstance(html_outpath, str)
outpath = os.path.join(html_outpath, wxid)
if os.path.exists(outpath):
shutil.rmtree(outpath, ignore_errors=True)
# 复制pywxdump/ui/web/*到outpath
web_path = os.path.join(os.path.dirname(os.path.dirname(__file__)), "ui", "web")
shutil.copytree(web_path, outpath)
code, ret = export_html(wxid, outpath, db_config, my_wxid=my_wxid)
if code:
return ReJson(0, ret)
else:
@ -449,59 +503,110 @@ def get_export_json():
# end 导出聊天记录 *******************************************************************************************************
# start 聊天记录分析api **************************************************************************************************
class DateCountRequest(BaseModel):
wxid: str = ""
start_time: int = 0
end_time: int = 0
time_format: str = "%Y-%m-%d"
@rs_api.route('/api/rs/date_count', methods=["GET", 'POST'])
def get_date_count():
@rs_api.api_route('/date_count', methods=["GET", 'POST'])
def get_date_count(request: DateCountRequest):
"""
获取日期统计
:return:
"""
if request.method not in ["GET", "POST"]:
return ReJson(1003, msg="Unsupported method")
rq_data = request.json if request.method == "POST" else request.args
word = rq_data.get("wxid", "")
start_time = rq_data.get("start_time", 0)
end_time = rq_data.get("end_time", 0)
time_format = rq_data.get("time_format", "%Y-%m-%d")
wxid = request.wxid
start_time = request.start_time
end_time = request.end_time
time_format = request.time_format
my_wxid = get_conf(g.caf, g.at, "last")
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
db_config = get_conf(g.caf, my_wxid, "db_config")
db = DBHandler(db_config)
date_count = db.get_date_count(wxid=word, start_time=start_time, end_time=end_time, time_format=time_format)
db_config = gc.get_conf(my_wxid, "db_config")
db = DBHandler(db_config, my_wxid=my_wxid)
date_count = db.get_date_count(wxid=wxid, start_time=start_time, end_time=end_time, time_format=time_format)
return ReJson(0, date_count)
@rs_api.route('/api/rs/top_talker_count', methods=["GET", 'POST'])
def get_top_talker_count():
class TopTalkerCountRequest(BaseModel):
top: int = 10
start_time: int = 0
end_time: int = 0
@rs_api.api_route('/top_talker_count', methods=["GET", 'POST'])
def get_top_talker_count(request: TopTalkerCountRequest):
"""
获取最多聊天的人
:return:
"""
if request.method not in ["GET", "POST"]:
return ReJson(1003, msg="Unsupported method")
rq_data = request.json if request.method == "POST" else request.args
top = rq_data.get("top", 10)
start_time = rq_data.get("start_time", 0)
end_time = rq_data.get("end_time", 0)
top = request.top
start_time = request.start_time
end_time = request.end_time
my_wxid = get_conf(g.caf, g.at, "last")
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
db_config = get_conf(g.caf, my_wxid, "db_config")
date_count = DBHandler(db_config).get_top_talker_count(top=top, start_time=start_time, end_time=end_time)
db_config = gc.get_conf(my_wxid, "db_config")
date_count = DBHandler(db_config, my_wxid=my_wxid).get_top_talker_count(top=top, start_time=start_time,
end_time=end_time)
return ReJson(0, date_count)
@rs_api.route('/api/rs/wordcloud', methods=["GET", 'POST'])
class WordCloudRequest(BaseModel):
target: str = "signature"
@rs_api.api_route('/wordcloud', methods=["GET", 'POST'])
@error9999
def wordcloud():
pass
def get_wordcloud(request: WordCloudRequest):
try:
import jieba
except ImportError:
return ReJson(9999, body="jieba is required")
target = request.target
if not target:
return ReJson(1002, body="target is required")
my_wxid = gc.get_conf(gc.at, "last")
if not my_wxid: return ReJson(1001, body="my_wxid is required")
db_config = gc.get_conf(my_wxid, "db_config")
db = DBHandler(db_config, my_wxid=my_wxid)
if target == "signature":
users = db.get_user()
signature_list = []
for wxid, user in users.items():
ExtraBuf = user.get("ExtraBuf", {})
signature = ExtraBuf.get("个性签名", "") if ExtraBuf else ""
if signature:
signature_list.append(signature)
signature_str = " ".join(signature_list)
words = jieba.lcut(signature_str)
words = [word for word in words if len(word) > 1]
count_dict = dict(Counter(words))
return ReJson(0, count_dict)
elif target == "nickname":
users = db.get_user()
nickname_list = []
for wxid, user in users.items():
nickname = user.get("nickname", "")
if nickname:
nickname_list.append(nickname)
nickname_str = " ".join(nickname_list)
words = jieba.lcut(nickname_str)
words = [word for word in words if len(word) > 1]
count_dict = dict(Counter(words))
return ReJson(0, count_dict)
return ReJson(1002, body="target is required")
# end 聊天记录分析api ****************************************************************************************************
# 关于、帮助、设置 *******************************************************************************************************
@rs_api.route('/api/rs/check_update', methods=["GET", 'POST'])
@rs_api.api_route('/check_update', methods=["GET", 'POST'])
@error9999
def check_update():
"""
@ -527,7 +632,7 @@ def check_update():
return ReJson(9999, msg=str(e))
@rs_api.route('/api/rs/version', methods=["GET", 'POST'])
@rs_api.api_route('/version', methods=["GET", "POST"])
@error9999
def version():
"""
@ -537,7 +642,7 @@ def version():
return ReJson(0, pywxdump.__version__)
@rs_api.route('/api/rs/get_readme', methods=["GET", 'POST'])
@rs_api.api_route('/get_readme', methods=["GET", 'POST'])
@error9999
def get_readme():
"""
@ -554,11 +659,4 @@ def get_readme():
else:
return ReJson(2001, body="status_code is not 200")
# END 关于、帮助、设置 ***************************************************************************************************
@rs_api.route('/')
@error9999
def index():
return render_template('index.html')

View File

@ -16,8 +16,123 @@ from .rjson import ReJson
from functools import wraps
import logging
rs_loger = logging.getLogger("rs_api")
ls_loger = logging.getLogger("ls_api")
server_loger = logging.getLogger("server")
rs_loger = server_loger
ls_loger = server_loger
class ConfData(object):
_instances = None
def __new__(cls, *args, **kwargs):
if cls._instances:
return cls._instances
cls._instances = object.__new__(cls)
return cls._instances
def __init__(self):
self._work_path = None
self.conf_file = None
self.auto_setting = None
self.is_init = False
self.conf = {}
self.init()
@property
def cf(self):
if not self.is_init:
self.init()
return self.conf_file
@property
def work_path(self):
if not self.is_init:
self.init()
return self._work_path
@property
def at(self):
if not self.is_init:
self.init()
return self.auto_setting
def init(self):
self.is_init = False
work_path = os.getenv("PYWXDUMP_WORK_PATH")
conf_file = os.getenv("PYWXDUMP_CONF_FILE")
auto_setting = os.getenv("PYWXDUMP_AUTO_SETTING")
if work_path is None or conf_file is None or auto_setting is None:
return False
self._work_path = work_path
self.conf_file = conf_file
self.auto_setting = auto_setting
self.is_init = True
if not os.path.exists(self.conf_file):
self.set_conf(self.auto_setting, "last", "")
self.read_conf()
return True
def read_conf(self):
if not os.path.exists(self.conf_file):
return False
try:
with open(self.conf_file, 'r') as f:
conf = json.load(f)
self.conf = conf
return True
except FileNotFoundError:
logging.error(f"Session file not found: {self.conf_file}")
return False
except json.JSONDecodeError as e:
logging.error(f"Error decoding JSON file: {e}")
return False
def write_conf(self):
if not self.is_init:
self.init()
try:
with open(self.conf_file, 'w') as f:
json.dump(self.conf, f, indent=4, ensure_ascii=False)
return True
except Exception as e:
logging.error(f"Error writing to file: {e}")
return False
def set_conf(self, wxid, arg, value):
if not self.is_init:
self.init()
if wxid not in self.conf:
self.conf[wxid] = {}
if not isinstance(self.conf[wxid], dict):
self.conf[wxid] = {}
self.conf[wxid][arg] = value
self.write_conf()
def get_conf(self, wxid, arg):
if not self.is_init:
self.init()
return self.conf.get(wxid, {}).get(arg, None)
def get_local_wxids(self):
if not self.is_init:
self.init()
return list(self.conf.keys())
def get_db_config(self):
if not self.is_init:
self.init()
my_wxid = self.get_conf(self.at, "last")
return self.get_conf(my_wxid, "db_config")
gc: ConfData = ConfData()
def get_conf_local_wxid(conf_file):
@ -83,6 +198,16 @@ def set_conf(conf_file, wxid, arg, value):
return True
def is_port_in_use(_host, _port):
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
try:
s.bind((_host, _port))
except socket.error:
return True
return False
def validate_title(title):
"""
校验文件名是否合法
@ -106,6 +231,20 @@ def error9999(func):
return wrapper
def asyncError9999(func):
@wraps(func)
async def wrapper(*args, **kwargs):
try:
return await func(*args, **kwargs)
except Exception as e:
traceback_data = traceback.format_exc()
rdata = f"{traceback_data}"
# logging.error(rdata)
return ReJson(9999, body=f"{str(e)}\n{rdata}", error=str(e))
return wrapper
def gen_base64(path):
# 获取文件名后缀
extension = os.path.splitext(path)[1]

View File

@ -6,8 +6,9 @@
# Date: 2023/10/14
# -------------------------------------------------------------------------------
import argparse
import os
import sys
import time
import json
from pywxdump import *
import pywxdump
@ -289,7 +290,7 @@ class MainShowChatRecords(BaseSubMainClass):
print("[-] 输入数据库路径不存在")
return
start_falsk(merge_path=merge_path, wx_path=args.wx_path, key="", my_wxid=args.my_wxid, online=online)
start_server(merge_path=merge_path, wx_path=args.wx_path, my_wxid=args.my_wxid, online=online)
class MainExportChatRecords(BaseSubMainClass):
@ -327,7 +328,7 @@ class MainUi(BaseSubMainClass):
parser.add_argument("-p", '--port', metavar="", type=int, help="(可选)端口号", default=5000)
parser.add_argument("--online", help="(可选)是否在线查看(局域网查看)", default=False, action='store_true')
parser.add_argument("--debug", help="(可选)是否开启debug模式", default=False, action='store_true')
parser.add_argument("--noOpenBrowser", dest='isOpenBrowser', action='store_false', default=True,
parser.add_argument("--noOpenBrowser", dest='isOpenBrowser', default=True, action='store_false',
help="(可选)用于禁用自动打开浏览器")
return parser
@ -339,7 +340,7 @@ class MainUi(BaseSubMainClass):
debug = args.debug
isopenBrowser = args.isOpenBrowser
start_falsk(port=port, online=online, debug=debug, isopenBrowser=isopenBrowser)
start_server(port=port, online=online, debug=debug, isopenBrowser=isopenBrowser)
class MainApi(BaseSubMainClass):
@ -360,7 +361,7 @@ class MainApi(BaseSubMainClass):
port = args.port
debug = args.debug
start_falsk(port=port, online=online, debug=debug, isopenBrowser=False)
start_server(port=port, online=online, debug=debug, isopenBrowser=False)
def console_run():

View File

@ -1,12 +1,10 @@
# -*- coding: utf-8 -*-#
# -------------------------------------------------------------------------------
# Name: __init__.py.py
# Description:
# Name: __init__.py
# Description: db
# Author: xaoyaoo
# Date: 2024/04/15
# -------------------------------------------------------------------------------
import pandas as pd
from .utils import download_file, dat2img
from .dbFavorite import FavoriteHandler
@ -16,37 +14,73 @@ from .dbMedia import MediaHandler
from .dbOpenIMContact import OpenIMContactHandler
from .dbPublicMsg import PublicMsgHandler
from .dbOpenIMMedia import OpenIMMediaHandler
from .export.exportCSV import export_csv
from .export.exportJSON import export_json
from .dbSns import SnsHandler
class DBHandler(MicroHandler, MediaHandler, OpenIMContactHandler, PublicMsgHandler, OpenIMMediaHandler,
FavoriteHandler):
FavoriteHandler, SnsHandler):
_class_name = "DBHandler"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.MSG_exist = self.Msg_tables_exist()
self.Micro_exist = self.Micro_tables_exist()
self.Media_exist = self.Media_tables_exist()
self.OpenIMContact_exist = self.OpenIMContact_tables_exist()
self.PublicMsg_exist = self.PublicMSG_tables_exist()
self.OpenIMMedia_exist = self.OpenIMMedia_tables_exist()
self.Favorite_exist = self.Favorite_tables_exist()
def __init__(self, db_config, my_wxid, *args, **kwargs):
self.config = db_config
self.my_wxid = my_wxid
if self.MSG_exist: # 添加索引
self.Msg_add_index()
if self.PublicMsg_exist: # 添加索引
self.PublicMsg_add_index()
if self.Micro_exist: # 添加索引
self.Micro_add_index()
super().__init__(self.config)
# 加速查询索引
self.Micro_add_index()
self.Msg_add_index()
self.PublicMsg_add_index()
self.Media_add_index()
def get_user(self, word=None, wxids=None, labels=None):
"""
获取联系人列表
:param word: 搜索关键字
:param wxids: wxid列表
:param labels: 标签列表
:return: 联系人dict {wxid: {}}
"""
users = self.get_user_list(word=word, wxids=wxids, label_ids=labels)
if self.OpenIMContact_exist:
users.update(self.get_im_user_list(word=word, wxids=wxids))
users.update(self.get_im_user_list(word=word, wxids=wxids))
return users
def get_msgs(self, wxids: list or str = "", start_index=0, page_size=500, msg_type: str = "",
msg_sub_type: str = "", start_createtime=None, end_createtime=None):
"""
获取聊天记录列表
:param wxids:[ wxid]
:param start_index: 起始索引
:param page_size: 页大小
:param msg_type: 消息类型
:param msg_sub_type: 消息子类型
:param start_createtime: 开始时间
:param end_createtime: 结束时间
:return: 聊天记录列表 {"id": _id, "MsgSvrID": str(MsgSvrID), "type_name": type_name, "is_sender": IsSender,
"talker": talker, "room_name": StrTalker, "msg": msg, "src": src, "extra": {},
"CreateTime": CreateTime, }
"""
msgs0, wxid_list0 = self.get_msg_list(wxids=wxids, start_index=start_index, page_size=page_size,
msg_type=msg_type,
msg_sub_type=msg_sub_type, start_createtime=start_createtime,
end_createtime=end_createtime, my_talker=self.my_wxid)
msgs1, wxid_list1 = self.get_plc_msg_list(wxids=wxids, start_index=start_index, page_size=page_size,
msg_type=msg_type,
msg_sub_type=msg_sub_type, start_createtime=start_createtime,
end_createtime=end_createtime, my_talker=self.my_wxid)
msgs = msgs0 + msgs1
wxid_list = wxid_list0 + wxid_list1
users = self.get_user(wxids=wxid_list)
return msgs, users
def get_msgs_count(self, wxids: list = ""):
chat_count = self.get_m_msg_count(wxids)
chat_count1 = self.get_plc_msg_count(wxids)
# 合并两个字典相同key则将value相加
count = {k: chat_count.get(k, 0) + chat_count1.get(k, 0) for k in
list(set(list(chat_count.keys()) + list(chat_count1.keys())))}
return count
__all__ = ["DBHandler", "FavoriteHandler", "MsgHandler", "MicroHandler", "MediaHandler",
"OpenIMContactHandler", "PublicMsgHandler", "OpenIMMediaHandler", "SnsHandler"]

View File

@ -1,11 +1,11 @@
# -*- coding: utf-8 -*-#
# -------------------------------------------------------------------------------
# Name: parsingFavorite.py
# Description:
# Name: Favorite.py
# Description: 负责处理wx收藏数据库
# Author: xaoyaoo
# Date: 2024/05/18
# -------------------------------------------------------------------------------
import pandas as pd
from collections import defaultdict
from .dbbase import DatabaseBase
from .utils import timestamp2str, xml2dict
@ -22,16 +22,12 @@ class FavoriteHandler(DatabaseBase):
_class_name = "Favorite"
Favorite_required_tables = ["FavItems", "FavDataItem", "FavTagDatas", "FavBindTagDatas"]
def Favorite_tables_exist(self):
"""
判断该类所需要的表是否存在
"""
return self.check_tables_exist(self.Favorite_required_tables)
def get_tags(self, LocalID):
"""
return: {LocalID: TagName}
"""
if not self.tables_exist("FavTagDatas"):
return {}
if LocalID is None:
sql = "select LocalID, TagName from FavTagDatas order by ServerSeq"
else:
@ -45,7 +41,8 @@ class FavoriteHandler(DatabaseBase):
"""
return: [(FavLocalID, TagName)]
"""
sql = "select A.FavLocalID, B.TagName from FavBindTagDatas A, FavTagDatas B where A.TagLocalID = B.LocalID"
sql = ("select DISTINCT A.FavLocalID, B.TagName "
"from FavBindTagDatas A, FavTagDatas B where A.TagLocalID = B.LocalID")
FavBindTags = self.execute(sql)
return FavBindTags
@ -108,6 +105,9 @@ class FavoriteHandler(DatabaseBase):
"Rerserved7": "保留字段7"
}
if not self.tables_exist(["FavItems", "FavDataItem"]):
return False
sql1 = "select " + ",".join(FavItemsFields.keys()) + " from FavItems order by UpdateTime desc"
sql2 = "select " + ",".join(FavDataItemFields.keys()) + " from FavDataItem B order by B.RecId asc"
@ -129,11 +129,26 @@ class FavoriteHandler(DatabaseBase):
for FavLocalID, TagName in FavTags:
FavTagsDict[FavLocalID] = FavTagsDict.get(FavLocalID, []) + [TagName]
rdata = []
for item in FavItemsList:
processed_item = {
key: item[i] for i, key in enumerate(FavItemsFields.keys())
}
processed_item['UpdateTime'] = timestamp2str(processed_item['UpdateTime'])
processed_item['XmlBuf'] = xml2dict(processed_item['XmlBuf'])
processed_item['TypeName'] = Favorite_type_converter(processed_item['Type'])
processed_item['FavData'] = FavDataDict.get(processed_item['FavLocalID'], [])
processed_item['Tags'] = FavTagsDict.get(processed_item['FavLocalID'], [])
rdata.append(processed_item)
try:
import pandas as pd
except ImportError:
return False
pf = pd.DataFrame(FavItemsList)
pf.columns = FavItemsFields.keys() # set column names
pf["UpdateTime"] = pf["UpdateTime"].apply(timestamp2str) # 处理时间
pf["XmlBuf"] = pf["XmlBuf"].apply(xml2dict) # 处理xml
pf["TypeName"] = pf["Type"].apply(FavoriteTypeId2Name) # 添加类型名称列
pf["TypeName"] = pf["Type"].apply(Favorite_type_converter) # 添加类型名称列
pf["FavData"] = pf["FavLocalID"].apply(lambda x: FavDataDict.get(x, [])) # 添加数据列
pf["Tags"] = pf["FavLocalID"].apply(lambda x: FavTagsDict.get(x, [])) # 添加标签列
pf = pf.fillna("") # 去掉Nan
@ -141,8 +156,15 @@ class FavoriteHandler(DatabaseBase):
return rdata
def FavoriteTypeId2Name(Type):
TypeNameDict = {
def Favorite_type_converter(type_id_or_name: [str, int]):
"""
收藏类型ID与名称转换
名称(str)=>ID(int)
ID(int)=>名称(str)
:param type_id_or_name: 消息类型ID或名称
:return: 消息类型名称或ID
"""
type_name_dict = defaultdict(lambda: "未知", {
1: "文本", # 文本 已测试
2: "图片", # 图片 已测试
3: "语音", # 语音
@ -154,5 +176,11 @@ def FavoriteTypeId2Name(Type):
14: "聊天记录", # 聊天记录 已测试
16: "群聊视频", # 群聊中的视频 可能
18: "笔记" # 笔记 已测试
}
return TypeNameDict.get(Type, "未知")
})
if isinstance(type_id_or_name, int):
return type_name_dict[type_id_or_name]
elif isinstance(type_id_or_name, str):
return next((k for k, v in type_name_dict.items() if v == type_id_or_name), (0, 0))
else:
raise ValueError("Invalid input type")

View File

@ -1,54 +1,43 @@
# -*- coding: utf-8 -*-#
# -------------------------------------------------------------------------------
# Name: parsingMSG.py
# Description:
# Name: MSG.py
# Description: 负责处理消息数据库数据
# Author: xaoyaoo
# Date: 2024/04/15
# -------------------------------------------------------------------------------
import json
import os
import re
# import time
# import pandas as pd
from .dbbase import DatabaseBase
from .utils import db_error, timestamp2str, xml2dict, match_BytesExtra, type_converter, \
get_md5, name2typeid, db_loger
import lz4.block
import blackboxprotobuf
from .dbbase import DatabaseBase
from .utils import db_error, timestamp2str, xml2dict, match_BytesExtra, type_converter
class MsgHandler(DatabaseBase):
_class_name = "MSG"
MSG_required_tables = ["MSG"]
def Msg_tables_exist(self):
"""
判断该类所需要的表是否存在
"""
return self.check_tables_exist(self.MSG_required_tables)
def Msg_add_index(self):
"""
添加索引,加快查询速度
"""
# 检查是否存在索引
sql = "CREATE INDEX IF NOT EXISTS idx_MSG_StrTalker ON MSG(StrTalker);"
self.execute(sql)
sql = "CREATE INDEX IF NOT EXISTS idx_MSG_CreateTime ON MSG(CreateTime);"
self.execute(sql)
sql = "CREATE INDEX IF NOT EXISTS idx_MSG_StrTalker_CreateTime ON MSG(StrTalker, CreateTime);"
self.execute(sql)
if not self.tables_exist("MSG"):
return
self.execute("CREATE INDEX IF NOT EXISTS idx_MSG_StrTalker ON MSG(StrTalker);")
self.execute("CREATE INDEX IF NOT EXISTS idx_MSG_CreateTime ON MSG(CreateTime);")
self.execute("CREATE INDEX IF NOT EXISTS idx_MSG_StrTalker_CreateTime ON MSG(StrTalker, CreateTime);")
@db_error
def get_msg_count(self, wxids: list = ""):
def get_m_msg_count(self, wxids: list = ""):
"""
获取聊天记录数量,根据wxid获取单个联系人的聊天记录数量不传wxid则获取所有联系人的聊天记录数量
:param wxids: wxid list
:return: 聊天记录数量列表 {wxid: chat_count}
:return: 聊天记录数量列表 {wxid: chat_count, total: total_count}
"""
if isinstance(wxids, str):
if isinstance(wxids, str) and wxids:
wxids = [wxids]
if wxids:
wxids = "('" + "','".join(wxids) + "')"
@ -57,6 +46,8 @@ class MsgHandler(DatabaseBase):
sql = f"SELECT StrTalker, COUNT(*) FROM MSG GROUP BY StrTalker ORDER BY COUNT(*) DESC;"
sql_total = f"SELECT COUNT(*) FROM MSG;"
if not self.tables_exist("MSG"):
return {}
result = self.execute(sql)
total_ret = self.execute(sql_total)
@ -70,9 +61,146 @@ class MsgHandler(DatabaseBase):
msg_count.update({row[0]: row[1] for row in result})
return msg_count
@db_error
def get_msg_list(self, wxids: list or str = "", start_index=0, page_size=500, msg_type: str = "",
msg_sub_type: str = "", start_createtime=None, end_createtime=None, my_talker=""):
"""
获取聊天记录列表
:param wxids: [wxid]
:param start_index: 起始索引
:param page_size: 页大小
:param msg_type: 消息类型
:param msg_sub_type: 消息子类型
:param start_createtime: 开始时间
:param end_createtime: 结束时间
:param my_talker:
:return: 聊天记录列表 {"id": _id, "MsgSvrID": str(MsgSvrID), "type_name": type_name, "is_sender": IsSender,
"talker": talker, "room_name": StrTalker, "msg": msg, "src": src, "extra": {},
"CreateTime": CreateTime, }
"""
if not self.tables_exist("MSG"):
return [], []
if isinstance(wxids, str) and wxids:
wxids = [wxids]
param = ()
sql_wxid, param = (f"AND StrTalker in ({', '.join('?' for _ in wxids)}) ",
param + tuple(wxids)) if wxids else ("", param)
sql_type, param = ("AND Type=? ", param + (msg_type,)) if msg_type else ("", param)
sql_sub_type, param = ("AND SubType=? ", param + (msg_sub_type,)) if msg_type and msg_sub_type else ("", param)
sql_start_createtime, param = ("AND CreateTime>=? ", param + (start_createtime,)) if start_createtime else (
"", param)
sql_end_createtime, param = ("AND CreateTime<=? ", param + (end_createtime,)) if end_createtime else ("", param)
sql = (
"SELECT localId,TalkerId,MsgSvrID,Type,SubType,CreateTime,IsSender,Sequence,StatusEx,FlagEx,Status,"
"MsgSequence,StrContent,MsgServerSeq,StrTalker,DisplayContent,Reserved0,Reserved1,Reserved3,"
"Reserved4,Reserved5,Reserved6,CompressContent,BytesExtra,BytesTrans,Reserved2,"
"ROW_NUMBER() OVER (ORDER BY CreateTime ASC) AS id "
"FROM MSG WHERE 1=1 "
f"{sql_wxid}"
f"{sql_type}"
f"{sql_sub_type}"
f"{sql_start_createtime}"
f"{sql_end_createtime}"
f"ORDER BY CreateTime ASC LIMIT ?,?"
)
param = param + (start_index, page_size)
result = self.execute(sql, param)
if not result:
return [], []
result_data = (self.get_msg_detail(row, my_talker=my_talker) for row in result)
rdata = list(result_data) # 转为列表
wxid_list = {d['talker'] for d in rdata} # 创建一个无重复的 wxid 列表
return rdata, list(wxid_list)
@db_error
def get_date_count(self, wxid='', start_time: int = 0, end_time: int = 0, time_format='%Y-%m-%d'):
"""
获取每日聊天记录数量包括发送者数量接收者数量和总数
"""
if not self.tables_exist("MSG"):
return {}
if isinstance(start_time, str) and start_time.isdigit():
start_time = int(start_time)
if isinstance(end_time, str) and end_time.isdigit():
end_time = int(end_time)
# if start_time or end_time is not an integer and not a float, set both to 0
if not (isinstance(start_time, (int, float)) and isinstance(end_time, (int, float))):
start_time = 0
end_time = 0
params = ()
sql_wxid = "AND StrTalker = ? " if wxid else ""
params = params + (wxid,) if wxid else params
sql_time = "AND CreateTime BETWEEN ? AND ? " if start_time and end_time else ""
params = params + (start_time, end_time) if start_time and end_time else params
sql = (f"SELECT strftime('{time_format}', CreateTime, 'unixepoch', 'localtime') AS date, "
" COUNT(*) AS total_count ,"
" SUM(CASE WHEN IsSender = 1 THEN 1 ELSE 0 END) AS sender_count, "
" SUM(CASE WHEN IsSender = 0 THEN 1 ELSE 0 END) AS receiver_count "
"FROM MSG "
"WHERE StrTalker NOT LIKE '%chatroom%' "
f"{sql_wxid} {sql_time} "
f"GROUP BY date ORDER BY date ASC;")
result = self.execute(sql, params)
if not result:
return {}
# 将查询结果转换为字典
result_dict = {}
for row in result:
date, total_count, sender_count, receiver_count = row
result_dict[date] = {
"sender_count": sender_count,
"receiver_count": receiver_count,
"total_count": total_count
}
return result_dict
@db_error
def get_top_talker_count(self, top: int = 10, start_time: int = 0, end_time: int = 0):
"""
获取聊天记录数量最多的联系人,他们聊天记录数量
"""
if not self.tables_exist("MSG"):
return {}
if isinstance(start_time, str) and start_time.isdigit():
start_time = int(start_time)
if isinstance(end_time, str) and end_time.isdigit():
end_time = int(end_time)
# if start_time or end_time is not an integer and not a float, set both to 0
if not (isinstance(start_time, (int, float)) and isinstance(end_time, (int, float))):
start_time = 0
end_time = 0
sql_time = f"AND CreateTime BETWEEN {start_time} AND {end_time} " if start_time and end_time else ""
sql = (
"SELECT StrTalker, COUNT(*) AS count,"
"SUM(CASE WHEN IsSender = 1 THEN 1 ELSE 0 END) AS sender_count, "
"SUM(CASE WHEN IsSender = 0 THEN 1 ELSE 0 END) AS receiver_count "
"FROM MSG "
"WHERE StrTalker NOT LIKE '%chatroom%' "
f"{sql_time} "
"GROUP BY StrTalker ORDER BY count DESC "
f"LIMIT {top};"
)
result = self.execute(sql)
if not result:
return {}
# 将查询结果转换为字典
result_dict = {row[0]: {"total_count": row[1], "sender_count": row[2], "receiver_count": row[3]} for row in
result}
return result_dict
# 单条消息处理
@db_error
def get_msg_detail(self, row):
def get_msg_detail(self, row, my_talker=""):
"""
获取单条消息详情,格式化输出
"""
@ -87,6 +215,7 @@ class MsgHandler(DatabaseBase):
msg = StrContent
src = ""
extra = {}
if type_id == (1, 0): # 文本
msg = StrContent
@ -112,7 +241,7 @@ class MsgHandler(DatabaseBase):
voicelength = int(voicelength) / 1000
voicelength = f"{voicelength:.2f}"
msg = f"语音时长:{voicelength}\n翻译结果:{transtext}" if transtext else f"语音时长:{voicelength}"
src = os.path.join("audio", f"{StrTalker}",
src = os.path.join(f"{StrTalker}",
f"{CreateTime.replace(':', '-').replace(' ', '_')}_{IsSender}_{MsgSvrID}.wav")
elif type_id == (43, 0): # 视频
DictExtra = get_BytesExtra(BytesExtra)
@ -161,8 +290,9 @@ class MsgHandler(DatabaseBase):
title = appmsg.get("title", "")
des = appmsg.get("des", "")
url = appmsg.get("url", "")
msg = f'{title}\n{des}\n\n<a href="{url}">点击查看详情</a>'
msg = f'{title}\n{des}\n\n<a href="{url}" target="_blank">点击查看详情</a>'
src = url
extra = appmsg
elif type_id == (49, 19): # 合并转发的聊天记录
CompressContent = decompress_CompressContent(CompressContent)
@ -234,7 +364,7 @@ class MsgHandler(DatabaseBase):
talker = "未知"
if IsSender == 1:
talker = ""
talker = my_talker
else:
if StrTalker.endswith("@chatroom"):
bytes_extra = get_BytesExtra(BytesExtra)
@ -249,126 +379,10 @@ class MsgHandler(DatabaseBase):
talker = StrTalker
row_data = {"id": _id, "MsgSvrID": str(MsgSvrID), "type_name": type_name, "is_sender": IsSender,
"talker": talker, "room_name": StrTalker, "msg": msg, "src": src, "extra": {},
"talker": talker, "room_name": StrTalker, "msg": msg, "src": src, "extra": extra,
"CreateTime": CreateTime, }
return row_data
@db_error
def get_msg_list(self, wxid="", start_index=0, page_size=500, msg_type: str = "", msg_sub_type: str = "",
start_createtime=None, end_createtime=None):
sql_base = ("SELECT localId,TalkerId,MsgSvrID,Type,SubType,CreateTime,IsSender,Sequence,StatusEx,FlagEx,Status,"
"MsgSequence,StrContent,MsgServerSeq,StrTalker,DisplayContent,Reserved0,Reserved1,Reserved3,"
"Reserved4,Reserved5,Reserved6,CompressContent,BytesExtra,BytesTrans,Reserved2,"
"ROW_NUMBER() OVER (ORDER BY CreateTime ASC) AS id "
"FROM MSG ")
param = ()
sql_wxid, param = ("AND StrTalker=? ", param + (wxid,)) if wxid else ("", param)
sql_type, param = ("AND Type=? ", param + (msg_type,)) if msg_type else ("", param)
sql_sub_type, param = ("AND SubType=? ", param + (msg_sub_type,)) if msg_type and msg_sub_type else ("", param)
sql_start_createtime, param = ("AND CreateTime>=? ", param + (start_createtime,)) if start_createtime else (
"", param)
sql_end_createtime, param = ("AND CreateTime<=? ", param + (end_createtime,)) if end_createtime else ("", param)
sql = (
f"{sql_base} WHERE 1=1 "
f"{sql_wxid}"
f"{sql_type}"
f"{sql_sub_type}"
f"{sql_start_createtime}"
f"{sql_end_createtime}"
f"ORDER BY CreateTime ASC LIMIT ?,?"
)
param = param + (start_index, page_size)
result = self.execute(sql, param)
if not result:
return [], []
result_data = (self.get_msg_detail(row) for row in result)
rdata = list(result_data) # 转为列表
wxid_list = {d['talker'] for d in rdata} # 创建一个无重复的 wxid 列表
return rdata, list(wxid_list)
@db_error
def get_date_count(self, wxid='', start_time: int = 0, end_time: int = 0, time_format='%Y-%m-%d'):
"""
获取每日聊天记录数量包括发送者数量接收者数量和总数
"""
if isinstance(start_time, str) and start_time.isdigit():
start_time = int(start_time)
if isinstance(end_time, str) and end_time.isdigit():
end_time = int(end_time)
# if start_time or end_time is not an integer and not a float, set both to 0
if not (isinstance(start_time, (int, float)) and isinstance(end_time, (int, float))):
start_time = 0
end_time = 0
params = ()
sql_wxid = "AND StrTalker = ? " if wxid else ""
params = params + (wxid,) if wxid else params
sql_time = "AND CreateTime BETWEEN ? AND ? " if start_time and end_time else ""
params = params + (start_time, end_time) if start_time and end_time else params
sql = (f"SELECT strftime('{time_format}', CreateTime, 'unixepoch', 'localtime') AS date, "
" COUNT(*) AS total_count ,"
" SUM(CASE WHEN IsSender = 1 THEN 1 ELSE 0 END) AS sender_count, "
" SUM(CASE WHEN IsSender = 0 THEN 1 ELSE 0 END) AS receiver_count "
"FROM MSG "
"WHERE StrTalker NOT LIKE '%chatroom%' "
f"{sql_wxid} {sql_time} "
f"GROUP BY date ORDER BY date ASC;")
result = self.execute(sql, params)
if not result:
return {}
# 将查询结果转换为字典
result_dict = {}
for row in result:
date, total_count, sender_count, receiver_count = row
result_dict[date] = {
"sender_count": sender_count,
"receiver_count": receiver_count,
"total_count": total_count
}
return result_dict
@db_error
def get_top_talker_count(self, top: int = 10, start_time: int = 0, end_time: int = 0):
"""
获取聊天记录数量最多的联系人,他们聊天记录数量
"""
if isinstance(start_time, str) and start_time.isdigit():
start_time = int(start_time)
if isinstance(end_time, str) and end_time.isdigit():
end_time = int(end_time)
# if start_time or end_time is not an integer and not a float, set both to 0
if not (isinstance(start_time, (int, float)) and isinstance(end_time, (int, float))):
start_time = 0
end_time = 0
sql_time = f"AND CreateTime BETWEEN {start_time} AND {end_time} " if start_time and end_time else ""
sql = (
"SELECT StrTalker, COUNT(*) AS count,"
"SUM(CASE WHEN IsSender = 1 THEN 1 ELSE 0 END) AS sender_count, "
"SUM(CASE WHEN IsSender = 0 THEN 1 ELSE 0 END) AS receiver_count "
"FROM MSG "
"WHERE StrTalker NOT LIKE '%chatroom%' "
f"{sql_time} "
"GROUP BY StrTalker ORDER BY count DESC "
f"LIMIT {top};"
)
result = self.execute(sql)
if not result:
return {}
# 将查询结果转换为字典
result_dict = {row[0]: {"total_count": row[1], "sender_count": row[2], "receiver_count": row[3]} for row in
result}
return result_dict
@db_error
def decompress_CompressContent(data):

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-#
# -------------------------------------------------------------------------------
# Name: MediaMSG_parsing.py
# Description:
# Name: MediaMSG.py
# Description: 负责处理语音数据库
# Author: xaoyaoo
# Date: 2024/04/15
# -------------------------------------------------------------------------------
@ -13,13 +13,17 @@ class MediaHandler(DatabaseBase):
_class_name = "MediaMSG"
Media_required_tables = ["Media"]
def Media_tables_exist(self):
def Media_add_index(self):
"""
判断该类所需要的表是否存在
添加索引, 加快查询速度
"""
return self.check_tables_exist(self.Media_required_tables)
if self.tables_exist("Media"):
self.execute("CREATE INDEX IF NOT EXISTS MsgSvrID ON Media(Reserved0)")
def get_audio(self, MsgSvrID, is_play=False, is_wave=False, save_path=None, rate=24000):
if not self.tables_exist("Media"):
return False
sql = "select Buf from Media where Reserved0=? "
DBdata = self.execute(sql, (MsgSvrID,))
if not DBdata:

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-#
# -------------------------------------------------------------------------------
# Name: parsingMicroMsg.py
# Description:
# Name: MicroMsg.py
# Description: 负责处理联系人数据库
# Author: xaoyaoo
# Date: 2024/04/15
# -------------------------------------------------------------------------------
@ -18,39 +18,41 @@ class MicroHandler(DatabaseBase):
Micro_required_tables = ["ContactLabel", "Contact", "ContactHeadImgUrl", "Session", "ChatInfo", "ChatRoom",
"ChatRoomInfo"]
def Micro_tables_exist(self):
"""
判断该类所需要的表是否存在
"""
return self.check_tables_exist(self.Micro_required_tables)
def Micro_add_index(self):
"""
添加索引, 加快查询速度
"""
# 为 Session 表添加索引
self.execute("CREATE INDEX IF NOT EXISTS idx_Session_strUsrName_nTime ON Session(strUsrName, nTime);")
self.execute("CREATE INDEX IF NOT EXISTS idx_Session_nOrder ON Session(nOrder);")
self.execute("CREATE INDEX IF NOT EXISTS idx_Session_nTime ON Session(nTime);")
if self.tables_exist("Session"):
self.execute("CREATE INDEX IF NOT EXISTS idx_Session_strUsrName_nTime ON Session(strUsrName, nTime);")
self.execute("CREATE INDEX IF NOT EXISTS idx_Session_nOrder ON Session(nOrder);")
self.execute("CREATE INDEX IF NOT EXISTS idx_Session_nTime ON Session(nTime);")
# 为 Contact 表添加索引
self.execute("CREATE INDEX IF NOT EXISTS idx_Contact_UserName ON Contact(UserName);")
if self.tables_exist("Contact"):
self.execute("CREATE INDEX IF NOT EXISTS idx_Contact_UserName ON Contact(UserName);")
# 为 ContactHeadImgUrl 表添加索引
self.execute("CREATE INDEX IF NOT EXISTS idx_ContactHeadImgUrl_usrName ON ContactHeadImgUrl(usrName);")
if self.tables_exist('ContactHeadImgUrl'):
self.execute("CREATE INDEX IF NOT EXISTS idx_ContactHeadImgUrl_usrName ON ContactHeadImgUrl(usrName);")
# 为 ChatInfo 表添加索引
self.execute("CREATE INDEX IF NOT EXISTS idx_ChatInfo_Username_LastReadedCreateTime "
"ON ChatInfo(Username, LastReadedCreateTime);")
self.execute("CREATE INDEX IF NOT EXISTS idx_ChatInfo_LastReadedCreateTime ON ChatInfo(LastReadedCreateTime);")
if self.tables_exist('ChatInfo'):
self.execute("CREATE INDEX IF NOT EXISTS idx_ChatInfo_Username_LastReadedCreateTime "
"ON ChatInfo(Username, LastReadedCreateTime);")
self.execute(
"CREATE INDEX IF NOT EXISTS idx_ChatInfo_LastReadedCreateTime ON ChatInfo(LastReadedCreateTime);")
# 为 Contact 表添加复合索引
self.execute("CREATE INDEX IF NOT EXISTS idx_Contact_search "
"ON Contact(UserName, NickName, Remark, Alias, QuanPin, PYInitial, RemarkQuanPin, RemarkPYInitial);")
if self.tables_exist('Contact'):
self.execute("CREATE INDEX IF NOT EXISTS idx_Contact_search "
"ON Contact(UserName, NickName, Remark, Alias, QuanPin, PYInitial, RemarkQuanPin, RemarkPYInitial);")
# 为 ChatRoom 和 ChatRoomInfo 表添加索引
self.execute("CREATE INDEX IF NOT EXISTS idx_ChatRoom_ChatRoomName ON ChatRoom(ChatRoomName);")
self.execute("CREATE INDEX IF NOT EXISTS idx_ChatRoomInfo_ChatRoomName ON ChatRoomInfo(ChatRoomName);")
if self.tables_exist(['ChatRoomInfo', "ChatRoom"]):
self.execute("CREATE INDEX IF NOT EXISTS idx_ChatRoom_ChatRoomName ON ChatRoom(ChatRoomName);")
self.execute("CREATE INDEX IF NOT EXISTS idx_ChatRoomInfo_ChatRoomName ON ChatRoomInfo(ChatRoomName);")
@db_error
def get_labels(self, id_is_key=True):
@ -59,12 +61,13 @@ class MicroHandler(DatabaseBase):
:param id_is_key: id_is_key: True: id作为keyFalse: name作为key
:return:
"""
if not self.table_exist.get("ContactLabel", False):
return {}
labels = {}
if not self.tables_exist("ContactLabel"):
return labels
sql = "SELECT LabelId, LabelName FROM ContactLabel ORDER BY LabelName ASC;"
result = self.execute(sql)
if not result:
return []
return labels
if id_is_key:
labels = {row[0]: row[1] for row in result}
else:
@ -78,6 +81,8 @@ class MicroHandler(DatabaseBase):
:return: 会话列表
"""
sessions = {}
if not self.tables_exist(["Session", "Contact", "ContactHeadImgUrl"]):
return sessions
sql = (
"SELECT S.strUsrName,S.nOrder,S.nUnReadCount, S.strNickName, S.nStatus, S.nIsSend, S.strContent, "
"S.nMsgLocalID, S.nMsgStatus, S.nTime, S.nMsgType, S.Reserved2 AS nMsgSubType, C.UserName, C.Alias, "
@ -90,6 +95,8 @@ class MicroHandler(DatabaseBase):
"WHERE S.strUsrName!='@publicUser' "
"ORDER BY S.nTime DESC;"
)
db_loger.info(f"get_session_list sql: {sql}")
ret = self.execute(sql)
if not ret:
return sessions
@ -124,6 +131,8 @@ class MicroHandler(DatabaseBase):
:return: 最近聊天的联系人
"""
users = []
if not self.tables_exist(["ChatInfo"]):
return users
sql = (
"SELECT A.Username, LastReadedCreateTime, LastReadedSvrId "
"FROM ( SELECT Username, MAX(LastReadedCreateTime) AS MaxLastReadedCreateTime FROM ChatInfo "
@ -132,6 +141,8 @@ class MicroHandler(DatabaseBase):
"ON A.Username = SubQuery.Username AND LastReadedCreateTime = SubQuery.MaxLastReadedCreateTime "
"ORDER BY A.LastReadedCreateTime DESC;"
)
db_loger.info(f"get_recent_chat_wxid sql: {sql}")
result = self.execute(sql)
if not result:
return []
@ -159,7 +170,8 @@ class MicroHandler(DatabaseBase):
label_ids = [label_ids]
users = {}
if not self.tables_exist(["Contact", "ContactHeadImgUrl"]):
return users
sql = (
"SELECT A.UserName, A.Alias, A.DelFlag, A.Type, A.VerifyFlag, A.Reserved1, A.Reserved2,"
"A.Remark, A.NickName, A.LabelIDList, A.ChatRoomType, A.ChatRoomNotify, A.Reserved5,"
@ -186,6 +198,7 @@ class MicroHandler(DatabaseBase):
sql_label = " OR ".join(sql_label)
sql = sql.replace(";", f"AND ({sql_label}) ;")
db_loger.info(f"get_user_list sql: {sql}")
result = self.execute(sql)
if not result:
return users
@ -205,7 +218,11 @@ class MicroHandler(DatabaseBase):
users[UserName] = {
"wxid": UserName, "nickname": NickName, "remark": Remark, "account": Alias,
"describe": describe, "headImgUrl": bigHeadImgUrl if bigHeadImgUrl else "",
"ExtraBuf": ExtraBuf, "LabelIDList": tuple(LabelIDList)}
"ExtraBuf": ExtraBuf, "LabelIDList": tuple(LabelIDList),
"extra": None}
extras = self.get_room_list(roomwxids=filter(lambda x: "@" in x, users.keys()))
for UserName in users:
users[UserName]["extra"] = extras.get(UserName, None)
return users
@db_error
@ -220,6 +237,9 @@ class MicroHandler(DatabaseBase):
if isinstance(roomwxids, str):
roomwxids = [roomwxids]
rooms = {}
if not self.tables_exist(["ChatRoom", "ChatRoomInfo"]):
return rooms
sql = (
"SELECT A.ChatRoomName,A.UserNameList,A.DisplayNameList,A.ChatRoomFlag,A.IsShowName,"
"A.SelfDisplayName,A.Reserved2,A.RoomData, "
@ -230,9 +250,9 @@ class MicroHandler(DatabaseBase):
sql = sql.replace(";",
f"AND A.ChatRoomName LIKE '%{word}%' ;")
if roomwxids:
sql = sql.replace(";", f"AND A.UserName IN ('" + "','".join(roomwxids) + "') ;")
sql = sql.replace(";", f"AND A.ChatRoomName IN ('" + "','".join(roomwxids) + "') ;")
rooms = {}
db_loger.info(f"get_room_list sql: {sql}")
result = self.execute(sql)
if not result:
return rooms
@ -247,7 +267,7 @@ class MicroHandler(DatabaseBase):
DisplayNameList = DisplayNameList.split("^G")
RoomData = ChatRoom_RoomData(RoomData)
wxid2remark = {}
wxid2roomNickname = {}
if RoomData:
rd = []
for k, v in RoomData.items():
@ -256,13 +276,20 @@ class MicroHandler(DatabaseBase):
for i in rd:
try:
if isinstance(i, dict) and isinstance(i.get('1'), str) and i.get('2'):
wxid2remark[i['1']] = i["2"]
wxid2roomNickname[i['1']] = i["2"]
except Exception as e:
db_loger.error(f"wxid2remark: ChatRoomName:{ChatRoomName}, {i} error:{e}", exc_info=True)
wxid2userinfo = self.get_user_list(wxids=UserNameList)
for i in wxid2userinfo:
wxid2userinfo[i]["roomNickname"] = wxid2roomNickname.get(i, "")
owner = wxid2userinfo.get(Reserved2, Reserved2)
rooms[ChatRoomName] = {
"wxid": ChatRoomName, "UserNameList": UserNameList, "DisplayNameList": DisplayNameList,
"ChatRoomFlag": ChatRoomFlag, "IsShowName": IsShowName, "SelfDisplayName": SelfDisplayName,
"owner": Reserved2, "wxid2remark": wxid2remark,
"wxid": ChatRoomName, "roomWxids": UserNameList, "IsShowName": IsShowName,
"ChatRoomFlag": ChatRoomFlag, "SelfDisplayName": SelfDisplayName,
"owner": owner, "wxid2userinfo": wxid2userinfo,
"Announcement": Announcement, "AnnouncementEditor": AnnouncementEditor,
"AnnouncementPublishTime": AnnouncementPublishTime}
return rooms

View File

@ -1,6 +1,6 @@
# -*- coding: utf-8 -*-#
# -------------------------------------------------------------------------------
# Name: parsingOpenIMContact.py
# Name: OpenIMContact.py
# Description:
# Author: xaoyaoo
# Date: 2024/04/16
@ -13,12 +13,6 @@ class OpenIMContactHandler(DatabaseBase):
_class_name = "OpenIMContact"
OpenIMContact_required_tables = ["OpenIMContact"]
def OpenIMContact_tables_exist(self):
"""
判断该类所需要的表是否存在
"""
return self.check_tables_exist(self.OpenIMContact_required_tables)
def get_im_user_list(self, word=None, wxids=None):
"""
获取联系人列表
@ -27,8 +21,10 @@ class OpenIMContactHandler(DatabaseBase):
:param wxids: 微信id列表
:return: 联系人字典
"""
if not self.tables_exist("OpenIMContact"):
return []
if not wxids:
wxids = []
wxids = {}
if isinstance(wxids, str):
wxids = [wxids]
sql = ("SELECT UserName,NickName,Type,Remark,BigHeadImgUrl,CustomInfoDetail,CustomInfoDetailVisible,"
@ -49,7 +45,7 @@ class OpenIMContactHandler(DatabaseBase):
result = self.execute(sql)
if not result:
return []
return {}
users = {}
for row in result:
@ -60,7 +56,7 @@ class OpenIMContactHandler(DatabaseBase):
users[UserName] = {
"wxid": UserName, "nickname": NickName, "remark": Remark, "account": UserName,
"describe": '', "headImgUrl": BigHeadImgUrl if BigHeadImgUrl else "",
"ExtraBuf": None, "LabelIDList": tuple()}
"ExtraBuf": None, "LabelIDList": tuple(), "extra": None}
return users

View File

@ -1,7 +1,7 @@
# -*- coding: utf-8 -*-#
# -------------------------------------------------------------------------------
# Name: MediaMSG_parsing.py
# Description:
# Name: OpenIMMedia.py
# Description: 负责处理语音数据库
# Author: xaoyaoo
# Date: 2024/04/15
# -------------------------------------------------------------------------------
@ -13,13 +13,9 @@ class OpenIMMediaHandler(DatabaseBase):
_class_name = "OpenIMMedia"
OpenIMMedia_required_tables = ["OpenIMMedia"]
def OpenIMMedia_tables_exist(self):
"""
判断该类所需要的表是否存在
"""
return self.check_tables_exist(self.OpenIMMedia_required_tables)
def get_im_audio(self, MsgSvrID, is_play=False, is_wave=False, save_path=None, rate=24000):
if not self.tables_exist("OpenIMMedia"):
return False
sql = "select Buf from OpenIMMedia where Reserved0=? "
DBdata = self.execute(sql, (MsgSvrID,))
if not DBdata:

View File

@ -1,54 +1,30 @@
# -*- coding: utf-8 -*-#
# -------------------------------------------------------------------------------
# Name: parsingPublicMsg.py
# Description:
# Name: PublicMsg.py
# Description: 负责处理公众号数据库信息
# Author: xaoyaoo
# Date: 2024/07/03
# -------------------------------------------------------------------------------
# -*- coding: utf-8 -*-#
# -------------------------------------------------------------------------------
# Name: parsingMSG.py
# Description:
# Author: xaoyaoo
# Date: 2024/04/15
# -------------------------------------------------------------------------------
import json
import os
import re
from typing import Union, Tuple
import pandas as pd
from .dbbase import DatabaseBase
from .dbMSG import MsgHandler
from .utils import get_md5, name2typeid, typeid2name, type_converter, timestamp2str, xml2dict, match_BytesExtra, \
db_error
import lz4.block
import blackboxprotobuf
from .utils import db_error
class PublicMsgHandler(MsgHandler):
_class_name = "PublicMSG"
PublicMSG_required_tables = ["PublicMsg"]
@db_error
def PublicMSG_tables_exist(self):
"""
判断该类所需要的表是否存在
"""
return self.check_tables_exist(self.PublicMSG_required_tables)
def PublicMsg_add_index(self):
"""
添加索引,加快查询速度
"""
# 检查是否存在索引
sql = "CREATE INDEX IF NOT EXISTS idx_PublicMsg_StrTalker ON MSG(StrTalker);"
if not self.tables_exist("PublicMsg"):
return
sql = "CREATE INDEX IF NOT EXISTS idx_PublicMsg_StrTalker ON PublicMsg(StrTalker);"
self.execute(sql)
sql = "CREATE INDEX IF NOT EXISTS idx_PublicMsg_CreateTime ON MSG(CreateTime);"
sql = "CREATE INDEX IF NOT EXISTS idx_PublicMsg_CreateTime ON PublicMsg(CreateTime);"
self.execute(sql)
sql = "CREATE INDEX IF NOT EXISTS idx_PublicMsg_StrTalker_CreateTime ON MSG(StrTalker, CreateTime);"
sql = "CREATE INDEX IF NOT EXISTS idx_PublicMsg_StrTalker_CreateTime ON PublicMsg(StrTalker, CreateTime);"
self.execute(sql)
@db_error
@ -58,7 +34,9 @@ class PublicMsgHandler(MsgHandler):
:param wxids: wxid list
:return: 聊天记录数量列表 {wxid: chat_count}
"""
if isinstance(wxids, str):
if not self.tables_exist("PublicMsg"):
return {}
if isinstance(wxids, str) and wxids:
wxids = [wxids]
if wxids:
wxids = "('" + "','".join(wxids) + "')"
@ -81,16 +59,29 @@ class PublicMsgHandler(MsgHandler):
return msg_count
@db_error
def get_plc_msg_list(self, wxid="", start_index=0, page_size=500, msg_type: str = "", msg_sub_type: str = "",
start_createtime=None, end_createtime=None):
sql_base = ("SELECT localId,TalkerId,MsgSvrID,Type,SubType,CreateTime,IsSender,Sequence,StatusEx,FlagEx,Status,"
"MsgSequence,StrContent,MsgServerSeq,StrTalker,DisplayContent,Reserved0,Reserved1,Reserved3,"
"Reserved4,Reserved5,Reserved6,CompressContent,BytesExtra,BytesTrans,Reserved2,"
"ROW_NUMBER() OVER (ORDER BY CreateTime ASC) AS id "
"FROM PublicMsg ")
def get_plc_msg_list(self, wxids: list or str = "", start_index=0, page_size=500, msg_type: str = "",
msg_sub_type: str = "", start_createtime=None, end_createtime=None, my_talker=""):
"""
获取聊天记录列表
:param wxids: [wxid]
:param start_index: 起始索引
:param page_size: 页大小
:param msg_type: 消息类型
:param msg_sub_type: 消息子类型
:param start_createtime: 开始时间
:param end_createtime: 结束时间
:return: 聊天记录列表 {"id": _id, "MsgSvrID": str(MsgSvrID), "type_name": type_name, "is_sender": IsSender,
"talker": talker, "room_name": StrTalker, "msg": msg, "src": src, "extra": {},
"CreateTime": CreateTime, }
"""
if not self.tables_exist("PublicMsg"):
return [], []
if isinstance(wxids, str) and wxids:
wxids = [wxids]
param = ()
sql_wxid, param = ("AND StrTalker=? ", param + (wxid,)) if wxid else ("", param)
sql_wxid, param = (f"AND StrTalker in ({', '.join('?' for _ in wxids)}) ",
param + tuple(wxids)) if wxids else ("", param)
sql_type, param = ("AND Type=? ", param + (msg_type,)) if msg_type else ("", param)
sql_sub_type, param = ("AND SubType=? ", param + (msg_sub_type,)) if msg_type and msg_sub_type else ("", param)
sql_start_createtime, param = ("AND CreateTime>=? ", param + (start_createtime,)) if start_createtime else (
@ -98,7 +89,11 @@ class PublicMsgHandler(MsgHandler):
sql_end_createtime, param = ("AND CreateTime<=? ", param + (end_createtime,)) if end_createtime else ("", param)
sql = (
f"{sql_base} WHERE 1=1 "
"SELECT localId,TalkerId,MsgSvrID,Type,SubType,CreateTime,IsSender,Sequence,StatusEx,FlagEx,Status,"
"MsgSequence,StrContent,MsgServerSeq,StrTalker,DisplayContent,Reserved0,Reserved1,Reserved3,"
"Reserved4,Reserved5,Reserved6,CompressContent,BytesExtra,BytesTrans,Reserved2,"
"ROW_NUMBER() OVER (ORDER BY CreateTime ASC) AS id "
"FROM PublicMsg WHERE 1=1 "
f"{sql_wxid}"
f"{sql_type}"
f"{sql_sub_type}"
@ -111,7 +106,7 @@ class PublicMsgHandler(MsgHandler):
if not result:
return [], []
result_data = (self.get_msg_detail(row) for row in result)
result_data = (self.get_msg_detail(row, my_talker=my_talker) for row in result)
rdata = list(result_data) # 转为列表
wxid_list = {d['talker'] for d in rdata} # 创建一个无重复的 wxid 列表

55
pywxdump/db/dbSns.py Normal file
View File

@ -0,0 +1,55 @@
# -*- coding: utf-8 -*-#
# -------------------------------------------------------------------------------
# Name: Sns.py
# Description: 负责处理朋友圈相关数据 软件只能看到在电脑微信浏览过的朋友圈记录
# Author: xaoyaoo
# Date: 2024/04/15
# -------------------------------------------------------------------------------
import json
from .dbbase import DatabaseBase
from .utils import silk2audio, xml2dict, timestamp2str
# FeedsV20朋友圈的XML数据
# CommentV20朋友圈点赞或评论记录
# NotificationV7朋友圈通知
# SnsConfigV20一些配置信息能读懂的是其中有你的朋友圈背景图
# SnsGroupInfoV5猜测是旧版微信朋友圈可见范围的可见或不可见名单
class SnsHandler(DatabaseBase):
_class_name = "Sns"
Media_required_tables = ["AdFeedsV8", "FeedsV20", "CommentV20", "NotificationV7", "SnsConfigV20", "SnsFailureV5",
"SnsGroupInfoV5", "SnsNoNotifyV5"]
def get_sns_feed(self):
"""
获取朋友圈数据
http://shmmsns.qpic.cn/mmsns/uGxMq1C4wvppcjBbyweK796GtT1hH3LGISYajZ2v7C11XhHk5icyDUXcWNSPk2MooeIa8Es5hXP0/0?idx=1&token=WSEN6qDsKwV8A02w3onOGQYfxnkibdqSOkmHhZGNB4DFumlE9p1vp0e0xjHoXlbbXRzwnQia6X5t3Annc4oqTuDg
"""
sql = (
"SELECT FeedId, CreateTime, FaultId, Type, UserName, Status, ExtFlag, PrivFlag, StringId, Content "
"FROM FeedsV20 "
"ORDER BY CreateTime DESC")
FeedsV20 = self.execute(sql)
for row in FeedsV20[2:]:
(FeedId, CreateTime, FaultId, Type, UserName, Status, ExtFlag, PrivFlag, StringId, Content) = row
Content = xml2dict(Content) if Content and Content.startswith("<") else Content
CreateTime = timestamp2str(CreateTime)
print(
f"{FeedId=}\n"
f"{CreateTime=}\n"
f"{FaultId=}\n"
f"{Type=}\n"
f"{UserName=}\n"
f"{Status=}\n"
f"{ExtFlag=}\n"
f"{PrivFlag=}\n"
f"{StringId=}\n\n"
f"{json.dumps(Content, indent=4, ensure_ascii=False)}\n\n"
)
return FeedId, CreateTime, FaultId, Type, UserName, Status, ExtFlag, PrivFlag, StringId, Content
def get_sns_comment(self):
pass

View File

@ -38,13 +38,13 @@ class DatabaseSingletonBase:
"""
if not db_config:
raise ValueError("db_config 不能为空")
db_key = db_config["key"]
db_type = db_config["type"]
db_key = db_config.get("key", "xaoyaoo_741852963")
db_type = db_config.get("type", "sqlite")
if db_key in cls._db_pool and cls._db_pool[db_key] is not None:
return cls._db_pool[db_key]
if db_type == "sqlite":
db_path = db_config["path"]
db_path = db_config.get("path", "")
if not os.path.exists(db_path):
raise FileNotFoundError(f"文件不存在: {db_path}")
pool = PooledDB(
@ -79,7 +79,7 @@ class DatabaseSingletonBase:
class DatabaseBase(DatabaseSingletonBase):
_class_name = "DatabaseBase"
table_exist = {}
existed_tables = []
def __init__(self, db_config):
"""
@ -91,6 +91,31 @@ class DatabaseBase(DatabaseSingletonBase):
"""
self.config = db_config
self.pool = self.connect(self.config)
self.__get_existed_tables()
def __get_existed_tables(self):
sql = "SELECT tbl_name FROM sqlite_master WHERE type = 'table' and tbl_name!='sqlite_sequence';"
existing_tables = self.execute(sql)
if existing_tables:
self.existed_tables = [row[0].lower() for row in existing_tables]
return self.existed_tables
else:
return None
def tables_exist(self, required_tables: str or list):
"""
判断该类所需要的表是否存在
Check if all required tables exist in the database.
Args:
required_tables (list or str): A list of table names or a single table name string.
Returns:
bool: True if all required tables exist, False otherwise.
"""
if isinstance(required_tables, str):
required_tables = [required_tables]
rbool = all(table.lower() in self.existed_tables for table in (required_tables or []))
if not rbool: db_loger.warning(f"{required_tables=}\n{self.existed_tables=}\n{rbool=}")
return rbool
def execute(self, sql, params=None):
"""
@ -125,20 +150,6 @@ class DatabaseBase(DatabaseSingletonBase):
finally:
connection.close()
def check_tables_exist(self, required_tables):
"""
判断该类所需要的表是否存在
"""
required_tables = required_tables or []
required_tables_str = "'" + "','".join(required_tables) + "'"
sql = (f"SELECT tbl_name FROM sqlite_master "
f"WHERE type='table' AND tbl_name in ({required_tables_str});")
existing_tables = self.execute(sql)
existing_tables = [row[0] for row in existing_tables] # 将查询结果转换为列表
self.table_exist = {table: table in existing_tables for table in required_tables}
# 检查所有必需的表是否都在现有表中
return all(table in existing_tables for table in required_tables)
def close(self):
self.pool.close()
db_loger.info(f"关闭数据库 - {self.config}")

View File

@ -1,10 +1,13 @@
# -*- coding: utf-8 -*-#
# -------------------------------------------------------------------------------
# Name: __init__.py.py
# Description:
# Description: db.utils
# Author: xaoyaoo
# Date: 2024/07/23
# -------------------------------------------------------------------------------
from ._loger import db_loger
from .common_utils import timestamp2str, xml2dict, silk2audio, bytes2str, get_md5, name2typeid, typeid2name, \
type_converter, match_BytesExtra, db_error, download_file, dat2img
__all__ = ["db_loger", "timestamp2str", "xml2dict", "silk2audio", "bytes2str", "get_md5", "name2typeid", "typeid2name",
"type_converter", "match_BytesExtra", "db_error", "download_file", "dat2img"]

View File

@ -252,18 +252,19 @@ def xml2dict(xml_string):
return parse_xml(root)
def download_file(url, save_path=None):
def download_file(url, save_path=None, proxies=None):
"""
下载文件
:param url: 文件下载地址
:param save_path: 保存路径
:param proxies: requests 代理
:return: 保存路径
"""
headers = {
"User-Agent": "Mozilla/5.0 (Linux; Android 10; Redmi K40 Pro) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/92.0.4515.159 Mobile Safari/537.36"
}
r = requests.get(url, headers=headers)
r = requests.get(url, headers=headers, proxies=proxies)
if r.status_code != 200:
return None
data = r.content

View File

@ -1,143 +0,0 @@
# -*- coding: utf-8 -*-#
# -------------------------------------------------------------------------------
# Name: server.py
# Description:
# Author: xaoyaoo
# Date: 2024/01/04
# -------------------------------------------------------------------------------
import os
import subprocess
import sys
import time
import logging
server_loger = logging.getLogger("server")
def is_port_in_use(_host, _port):
import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
try:
s.bind((_host, _port))
except socket.error:
return True
return False
def start_falsk(merge_path="", wx_path="", key="", my_wxid="", port=5000, online=False, debug=False,
isopenBrowser=True, loger_handler=None):
"""
启动flask
:param merge_path: 合并后的数据库路径
:param wx_path: 微信文件夹的路径用于显示图片
:param key: 密钥
:param my_wxid: 微信账号(本人微信id)
:param port: 端口号
:param online: 是否在线查看(局域网查看)
:param debug: 是否开启debug模式
:param isopenBrowser: 是否自动打开浏览器
:return:
"""
work_path = os.path.join(os.getcwd(), "wxdump_work") # 临时文件夹,用于存放图片等
if not os.path.exists(work_path):
os.makedirs(work_path)
server_loger.info(f"[+] 创建临时文件夹:{work_path}")
print(f"[+] 创建临时文件夹:{work_path}")
conf_auto_file = os.path.join(work_path, "conf_auto.json") # 用于存放各种基础信息
at = "auto_setting"
from flask import Flask, g
from flask_cors import CORS
from pywxdump.api import rs_api, ls_api, get_conf, set_conf
# 检查端口是否被占用
if online:
host = '0.0.0.0'
else:
host = "127.0.0.1"
app = Flask(__name__, template_folder='./ui/web', static_folder='./ui/web/assets/', static_url_path='/assets/')
with app.app_context():
# 设置超时时间为 1000 秒
app.config['TIMEOUT'] = 1000
app.secret_key = 'secret_key'
app.logger.setLevel(logging.WARNING)
if loger_handler:
app.logger.addHandler(loger_handler)
# 获取 Werkzeug 的日志记录器
werkzeug_logger = logging.getLogger('werkzeug')
# 将自定义格式器应用到 Werkzeug 的日志记录器
werkzeug_logger.addHandler(loger_handler)
werkzeug_logger.setLevel(logging.DEBUG)
CORS(app, resources={r"/*": {"origins": "*"}}, supports_credentials=True) # 允许所有域名跨域
@app.after_request # 请求后的处理 用于解决部分用户浏览器不支持flask以及vue的js文件返回问题
def changeHeader(response):
disposition = response.get_wsgi_headers('environ').get(
'Content-Disposition') or '' # 获取返回头文件名描述,如'inline; filename=index.562b9b5a.js'
if disposition.rfind('.js') == len(disposition) - 3:
response.mimetype = 'application/javascript'
return response
@app.before_request
def before_request():
g.work_path = work_path # 临时文件夹,用于存放图片等-新版本
g.caf = conf_auto_file # 用于存放各种基础信息-新版本
g.at = at # 用于默认设置-新版本
if merge_path:
set_conf(conf_auto_file, at, "merge_path", merge_path)
db_config = {
"key": "merge_all",
"type": "sqlite",
"path": "D:\\_code\\py_code\\pywxdumpProject\\z_test\\wxdump_work\\wxid_zh12s67kxsqs22\\merge_all.db"
}
set_conf(conf_auto_file, at, "db_config", db_config)
if wx_path: set_conf(conf_auto_file, at, "wx_path", wx_path)
if key: set_conf(conf_auto_file, at, "key", key)
if my_wxid: set_conf(conf_auto_file, at, "my_wxid", my_wxid)
if not os.path.exists(conf_auto_file):
set_conf(conf_auto_file, at, "last", my_wxid)
app.register_blueprint(rs_api)
app.register_blueprint(ls_api)
if isopenBrowser:
try:
# 自动打开浏览器
url = f"http://127.0.0.1:{port}/"
# 根据操作系统使用不同的命令打开默认浏览器
if sys.platform.startswith('darwin'): # macOS
subprocess.call(['open', url])
elif sys.platform.startswith('win'): # Windows
subprocess.call(['start', url], shell=True)
elif sys.platform.startswith('linux'): # Linux
subprocess.call(['xdg-open', url])
else:
server_loger.error(f"Unsupported platform, can't open browser automatically.", exc_info=True)
print("Unsupported platform, can't open browser automatically.")
except Exception as e:
server_loger.error(f"自动打开浏览器失败:{e}", exc_info=True)
if is_port_in_use(host, port):
server_loger.error(f"Port {port} is already in use. Choose a different port.")
print(f"Port {port} is already in use. Choose a different port.")
input("Press Enter to exit...")
else:
time.sleep(1)
server_loger.info(f"启动flask服务host:port{host}:{port}")
print("[+] 请使用浏览器访问 http://127.0.0.1:5000/ 查看聊天记录")
app.run(host=host, port=port, debug=debug, threaded=False)
if __name__ == '__main__':
merge_path = r"****.db"
wx_path = r"****"
my_wxid = "****"
start_falsk(merge_path=merge_path, wx_path=wx_path, my_wxid=my_wxid,
port=5000, online=False, debug=False, isopenBrowser=False)

View File

@ -1,71 +0,0 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>chat</title>
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/css/bootstrap.min.css">
<style>
img {
max-width: 400px;
}
</style>
</head>
<body>
<div>
<div class="row" style="background-color: #d3eed3">
<div style="overflow-y: auto;height:90vh;">
<table class="table">
<tbody>
{% for msg in msgs %}
<tr id="{{ msg.MsgSvrID }}">
{% if msg.is_sender == 1 %}
<div style="background-color: #f3e9c1;">
<label style="color:#A13A50">[{{msg.talker}}][{{msg.type_name}}] {{msg.CreateTime}}</label><br>
{% if msg.type_name == '语音' %}
<audio controls>
<source src="{{msg.content.src}}" type="audio/wav">
</audio>
{% elif msg.type_name == '图片' %}
<img src="{{msg.content.src}}" alt="{{msg.content.msg}}" style="{{msg.content.style}}"/>
{% elif msg.type_name == '动画表情' %}
<img src="{{msg.content.src}}" alt="{{msg.content.msg}}" style="{{msg.content.style}}"/>
{% else %}
<p>{{msg.content.msg}}</p>
{% endif %}
</div>
{% else %}
<div style="background-color: #d3eed3">
<label style="color:#f54f71">[{{msg.talker}}][{{msg.type_name}}] {{msg.CreateTime}}</label><br>
{% if msg.type_name == '语音' %}
<audio controls>
<source src="{{msg.content.src}}" type="audio/wav">
</audio>
{% elif msg.type_name == '图片' %}
<img src="{{msg.content.src}}" alt="{{msg.content.msg}}" style="{{msg.content.style}}"/>
{% elif msg.type_name == '动画表情' %}
<img src="{{msg.content.src}}" alt="{{msg.content.msg}}" style="{{msg.content.style}}"/>
{% else %}
<p>{{msg.content.msg}}</p>
{% endif %}
</div>
{% endif %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0/js/bootstrap.min.js"></script>
</body>
</html>

View File

@ -1,197 +0,0 @@
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<title>聊天记录显示</title>
<link rel="stylesheet" href="https://cdn.staticfile.org/bootstrap/4.5.3/css/bootstrap.min.css">
<style>
.left-area {
background-color: #f2f2f2;
height: 100vh;
}
</style>
</head>
<body>
<div class="container-fluid">
<div class="row">
<div class="col-3 left-area">
<div style="height:100vh; overflow-y: auto;">
<table class="table">
<thead>
<tr>
<th scope="col">名称</th>
<th scope="col">数量</th>
</tr>
</thead>
<tbody>
{% for user in users %}
<tr id="{{ user.username }}">
<td style="display: none;">
<username id="username1">{{user.username}}</username>
<nickname id="nickname1">{{user.nickname}}</nickname>
<remark id="remark1">{{user.remark}}</remark>
<chat_count id="chat_count1">{{user.chat_count}}</chat_count>
</td>
<td>
{% if user.remark not in [None, '']%}
{{user.remark}}
{% else %}
{{user.nickname}}
{% endif %}
</td>
<td>{{user.chat_count}}</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="col-9 right-area">
<div id="topdiv" class="row" style="background-color: #ccdcef; max-height: 120px;display: none;">
<div class="col-3">
账号:<span id="username" style="color: #2f6006;word-wrap: break-word;"></span>
</div>
<div class="col-4">
昵称:<span id="nickname" style="color: #4a5905;word-wrap: break-word;"></span>
</div>
<div class="col-3">
备注:<span id="remark" style="color: #b66a2f;word-wrap: break-word;"></span>
</div>
<div class="col-2">
消息数:<span id="chat_count" style="color: #f6062a;"></span>
</div>
</div>
<div id="pagination" class="row"
style="background-color: #ccdcef; max-height: 120px; display: flex; align-items: center; display: none;">
<div class="col-9" style="display: flex;">
<label class="page-link">
<a id="pre_page" class="" href="#">上一页</a>&nbsp;
<a id="next_page" class="" href="#">下一页</a>
&nbsp; &nbsp; &nbsp;
<input id="ipt_go" type="number" min="1" max="1000"
style="width: 80px; margin-right: 10px;"/>/<a id="all_pages"></a>&nbsp;
<a id="goButton" href="#">跳转</a></label>
</div>
<div class="col-3" style="display: flex; justify-content: flex-end;">
<button id="btn_export" type="button" class="btn btn-primary">导出</button>
</div>
</div>
<div class="init-right-area"
style="background-color: #e6e6e6; height: 100vh; display: grid; place-items: center; ">
<h2 style="text-align: center">欢迎使用<a href="https://github.com/xaoyaoo/PyWxDump.git">PyWxDump</a>聊天记录查看工具!
</h2>
</div>
</div>
</div>
</div>
<script src="https://code.jquery.com/jquery-3.2.1.slim.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.12.9/umd/popper.min.js"></script>
<script src="https://cdn.staticfile.org/bootstrap/4.5.3/js/bootstrap.min.js"></script>
<script>
var globalUsername = ''; // 全局变量
var globalNickname = ''; // 全局变量
var globalRemark = ''; // 全局变量
var globalChatCount = 0; // 全局变量
var globalLimit = 100; // 全局变量
var globalPages = Math.ceil(globalChatCount / globalLimit); // 全局变量
var globalCurrentPage = globalPages; // 全局变量
// 发送请求并更新右侧区域内容
var request_function = function (url) {
fetch(url, {
method: 'GET',
headers: {
'Content-Type': 'text/plain'
}
})
.then(response => response.text())
.then(data => {
document.querySelector('.init-right-area').style = '';
document.querySelector('.init-right-area').innerHTML = ''; // 清空右侧区域内容
document.querySelector('.init-right-area').innerHTML = data; // 更新右侧区域内容
document.getElementById('topdiv').style.display = "";
document.getElementById('username').innerHTML = globalUsername
document.getElementById('nickname').innerHTML = globalNickname;
document.getElementById('remark').innerHTML = globalRemark;
document.getElementById('chat_count').innerHTML = globalChatCount;
document.getElementById('pagination').style.display = 'flex';
document.getElementById('ipt_go').max = globalPages;
document.getElementById('ipt_go').value =globalCurrentPage;
document.getElementById('all_pages').innerHTML = globalPages;
});
};
// 为每行添加点击事件监听器
document.querySelectorAll('.left-area tbody tr').forEach(function (row) {
row.addEventListener('click', function () {
globalUsername = row.id; // 获取用户名
globalNickname = row.querySelector('#nickname1').innerHTML; // 获取昵称
globalRemark = row.querySelector('#remark1').innerHTML; // 获取备注
globalChatCount = row.querySelector('#chat_count1').innerHTML; // 获取消息数
globalLimit = 100; // 设置全局变量
globalPages = Math.ceil(globalChatCount / globalLimit); // 设置全局变量
globalCurrentPage = globalPages; // 设置全局变量
var requestUrl = '/get_chat_data?username=' + encodeURIComponent(globalUsername) + '&page=' + globalCurrentPage + '&limit=' + globalLimit;
// 发送请求并更新右侧区域内容
request_function(requestUrl);
})
;
});
// 上一页按钮点击事件
document.getElementById('pre_page').addEventListener('click', function () {
if (globalCurrentPage > 1) {
globalCurrentPage -= 1;
var requestUrl = '/get_chat_data?username=' + encodeURIComponent(globalUsername) + '&page=' + globalCurrentPage + '&limit=' + globalLimit;
// 发送请求并更新右侧区域内容
request_function(requestUrl);
}
});
// 下一页按钮点击事件
document.getElementById('next_page').addEventListener('click', function () {
if (globalCurrentPage < globalPages) {
globalCurrentPage += 1;
var requestUrl = '/get_chat_data?username=' + encodeURIComponent(globalUsername) + '&page=' + globalCurrentPage + '&limit=' + globalLimit;
// 发送请求并更新右侧区域内容
request_function(requestUrl);
}
});
// 跳转按钮点击事件
document.getElementById('goButton').addEventListener('click', function () {
var page = document.getElementById('ipt_go').value;
if (page > 0 && page <= globalPages) {
globalCurrentPage = page;
var requestUrl = '/get_chat_data?username=' + encodeURIComponent(globalUsername) + '&page=' + globalCurrentPage + '&limit=' + globalLimit;
// 发送请求并更新右侧区域内容
request_function(requestUrl);
}
});
// 导出按钮点击事件
document.getElementById('btn_export').addEventListener('click', function () {
var requestUrl = '/export_chat_data?username=' + encodeURIComponent(globalUsername);
window.open(requestUrl);
});
</script>
</body>
</html>

View File

@ -8,4 +8,4 @@
from .wx_info import get_wx_info, get_wx_db, get_core_db
from .get_bias_addr import BiasAddr
from .decryption import batch_decrypt, decrypt
from .merge_db import merge_db, decrypt_merge, merge_real_time_db, all_merge_real_time_db
from .merge_db import merge_db, decrypt_merge, merge_real_time_db, all_merge_real_time_db

View File

@ -1,274 +0,0 @@
import ctypes
import ctypes.wintypes
from collections import namedtuple
# 定义必要的常量
TH32CS_SNAPPROCESS = 0x00000002
MAX_PATH = 260
PROCESS_QUERY_INFORMATION = 0x0400
PROCESS_VM_READ = 0x0010
# MEMORY_BASIC_INFORMATION 结构体定义
class MEMORY_BASIC_INFORMATION(ctypes.Structure):
_fields_ = [
('BaseAddress', ctypes.wintypes.LPVOID),
('AllocationBase', ctypes.wintypes.LPVOID),
('AllocationProtect', ctypes.wintypes.DWORD),
('RegionSize', ctypes.c_size_t),
('State', ctypes.wintypes.DWORD),
('Protect', ctypes.wintypes.DWORD),
('Type', ctypes.wintypes.DWORD)
]
class MODULEINFO(ctypes.Structure):
_fields_ = [
("lpBaseOfDll", ctypes.c_void_p), # remote pointer
("SizeOfImage", ctypes.c_ulong),
("EntryPoint", ctypes.c_void_p), # remote pointer
]
# 定义PROCESSENTRY32结构
class PROCESSENTRY32(ctypes.Structure):
_fields_ = [("dwSize", ctypes.wintypes.DWORD),
("cntUsage", ctypes.wintypes.DWORD),
("th32ProcessID", ctypes.wintypes.DWORD),
("th32DefaultHeapID", ctypes.POINTER(ctypes.wintypes.ULONG)),
("th32ModuleID", ctypes.wintypes.DWORD),
("cntThreads", ctypes.wintypes.DWORD),
("th32ParentProcessID", ctypes.wintypes.DWORD),
("pcPriClassBase", ctypes.wintypes.LONG),
("dwFlags", ctypes.wintypes.DWORD),
("szExeFile", ctypes.c_char * MAX_PATH)]
class VS_FIXEDFILEINFO(ctypes.Structure):
_fields_ = [
('dwSignature', ctypes.wintypes.DWORD),
('dwStrucVersion', ctypes.wintypes.DWORD),
('dwFileVersionMS', ctypes.wintypes.DWORD),
('dwFileVersionLS', ctypes.wintypes.DWORD),
('dwProductVersionMS', ctypes.wintypes.DWORD),
('dwProductVersionLS', ctypes.wintypes.DWORD),
('dwFileFlagsMask', ctypes.wintypes.DWORD),
('dwFileFlags', ctypes.wintypes.DWORD),
('dwFileOS', ctypes.wintypes.DWORD),
('dwFileType', ctypes.wintypes.DWORD),
('dwFileSubtype', ctypes.wintypes.DWORD),
('dwFileDateMS', ctypes.wintypes.DWORD),
('dwFileDateLS', ctypes.wintypes.DWORD),
]
# 加载dll
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
psapi = ctypes.WinDLL('psapi', use_last_error=True)
version = ctypes.WinDLL('version', use_last_error=True)
# 创建进程快照
CreateToolhelp32Snapshot = kernel32.CreateToolhelp32Snapshot
CreateToolhelp32Snapshot.argtypes = [ctypes.wintypes.DWORD, ctypes.wintypes.DWORD]
CreateToolhelp32Snapshot.restype = ctypes.wintypes.HANDLE
# 获取第一个进程
Process32First = kernel32.Process32First
Process32First.argtypes = [ctypes.wintypes.HANDLE, ctypes.POINTER(PROCESSENTRY32)]
Process32First.restype = ctypes.wintypes.BOOL
# 获取下一个进程
Process32Next = kernel32.Process32Next
Process32Next.argtypes = [ctypes.wintypes.HANDLE, ctypes.POINTER(PROCESSENTRY32)]
Process32Next.restype = ctypes.wintypes.BOOL
# 关闭句柄
CloseHandle = kernel32.CloseHandle
CloseHandle.argtypes = [ctypes.wintypes.HANDLE]
CloseHandle.restype = ctypes.wintypes.BOOL
# 打开进程
OpenProcess = kernel32.OpenProcess
OpenProcess.argtypes = [ctypes.wintypes.DWORD, ctypes.wintypes.BOOL, ctypes.wintypes.DWORD]
OpenProcess.restype = ctypes.wintypes.HANDLE
# 获取模块文件名
GetModuleFileNameEx = psapi.GetModuleFileNameExA
GetModuleFileNameEx.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.HANDLE, ctypes.c_char_p, ctypes.wintypes.DWORD]
GetModuleFileNameEx.restype = ctypes.wintypes.DWORD
# 获取文件版本信息大小
GetFileVersionInfoSizeW = version.GetFileVersionInfoSizeW
GetFileVersionInfoSizeW.argtypes = [ctypes.wintypes.LPCWSTR, ctypes.POINTER(ctypes.wintypes.DWORD)]
GetFileVersionInfoSizeW.restype = ctypes.wintypes.DWORD
# 获取文件版本信息
GetFileVersionInfoW = version.GetFileVersionInfoW
GetFileVersionInfoW.argtypes = [ctypes.wintypes.LPCWSTR, ctypes.wintypes.DWORD, ctypes.wintypes.DWORD, ctypes.c_void_p]
GetFileVersionInfoW.restype = ctypes.wintypes.BOOL
# 查询文件版本信息
VerQueryValueW = version.VerQueryValueW
VerQueryValueW.argtypes = [ctypes.c_void_p, ctypes.wintypes.LPCWSTR, ctypes.POINTER(ctypes.c_void_p),
ctypes.POINTER(ctypes.wintypes.UINT)]
VerQueryValueW.restype = ctypes.wintypes.BOOL
# 获取模块信息
GetModuleInformation = psapi.GetModuleInformation
GetModuleInformation.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.HMODULE, ctypes.POINTER(MODULEINFO),
ctypes.wintypes.DWORD]
GetModuleInformation.restype = ctypes.c_bool
# 读取进程内存
ReadProcessMemory = ctypes.windll.kernel32.ReadProcessMemory
# 定义VirtualQueryEx函数
VirtualQueryEx = kernel32.VirtualQueryEx
VirtualQueryEx.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.LPCVOID, ctypes.POINTER(MEMORY_BASIC_INFORMATION),
ctypes.c_size_t]
VirtualQueryEx.restype = ctypes.c_size_t
# 获取映射文件名
GetMappedFileName = psapi.GetMappedFileNameA
GetMappedFileName.argtypes = [ctypes.wintypes.HANDLE, ctypes.wintypes.LPVOID, ctypes.c_char_p, ctypes.wintypes.DWORD]
GetMappedFileName.restype = ctypes.wintypes.DWORD
GetMappedFileNameW = psapi.GetMappedFileNameW
GetMappedFileNameW.restype = ctypes.wintypes.DWORD
GetMappedFileNameW.argtypes = [ctypes.wintypes.HANDLE, ctypes.c_void_p, ctypes.wintypes.LPWSTR, ctypes.wintypes.DWORD]
def get_info_with_key(h_process, address, address_len=8):
array = ctypes.create_string_buffer(address_len)
if ReadProcessMemory(h_process, ctypes.c_void_p(address), array, address_len, 0) == 0: return None
address = int.from_bytes(array, byteorder='little') # 逆序转换为int地址key地址
key = ctypes.create_string_buffer(32)
if ReadProcessMemory(h_process, ctypes.c_void_p(address), key, 32, 0) == 0: return None
key_string = bytes(key).hex()
return key_string
def get_memory_maps(pid):
# 打开进程
access = PROCESS_QUERY_INFORMATION | PROCESS_VM_READ
hProcess = OpenProcess(access, False, pid)
if not hProcess:
return []
memory_maps = []
base_address = 0
mbi = MEMORY_BASIC_INFORMATION()
max_address = 0x7FFFFFFFFFFFFFFF # 64位系统的最大地址
while base_address < max_address:
if VirtualQueryEx(hProcess, base_address, ctypes.byref(mbi), ctypes.sizeof(mbi)) == 0:
break
mapped_file_name = ctypes.create_unicode_buffer(ctypes.wintypes.MAX_PATH)
if GetMappedFileNameW(hProcess, base_address, mapped_file_name, ctypes.wintypes.MAX_PATH) > 0:
file_name = mapped_file_name.value
else:
file_name = None
# module_info = MODULEINFO()
# if GetModuleInformation(hProcess, mbi.BaseAddress, ctypes.byref(module_info), ctypes.sizeof(module_info)):
# file_name = get_file_version_info(module_info.lpBaseOfDll)
memory_maps.append({
'BaseAddress': mbi.BaseAddress,
'RegionSize': mbi.RegionSize,
'State': mbi.State,
'Protect': mbi.Protect,
'Type': mbi.Type,
'FileName': file_name
})
base_address += mbi.RegionSize
CloseHandle(hProcess)
MemMap = namedtuple('MemMap', ['BaseAddress', 'RegionSize', 'State', 'Protect', 'Type', 'FileName'])
return [MemMap(**m) for m in memory_maps]
def get_process_exe_path(process_id):
h_process = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, False, process_id)
if not h_process:
return None
exe_path = ctypes.create_string_buffer(MAX_PATH)
if GetModuleFileNameEx(h_process, None, exe_path, MAX_PATH) > 0:
CloseHandle(h_process)
return exe_path.value.decode('utf-8', errors='ignore')
else:
CloseHandle(h_process)
return None
def get_file_version_info(file_path):
size = GetFileVersionInfoSizeW(file_path, None)
if size == 0:
return None
res = ctypes.create_string_buffer(size)
if not GetFileVersionInfoW(file_path, 0, size, res):
return None
uLen = ctypes.wintypes.UINT()
lplpBuffer = ctypes.c_void_p()
if not VerQueryValueW(res, r'\\', ctypes.byref(lplpBuffer), ctypes.byref(uLen)):
return None
ffi = ctypes.cast(lplpBuffer, ctypes.POINTER(VS_FIXEDFILEINFO)).contents
if ffi.dwSignature != 0xFEEF04BD:
return None
version = (
(ffi.dwFileVersionMS >> 16) & 0xffff,
ffi.dwFileVersionMS & 0xffff,
(ffi.dwFileVersionLS >> 16) & 0xffff,
ffi.dwFileVersionLS & 0xffff,
)
# f"{version[0]}.{version[1]}.{version[2]}.{version[3]}"
return f"{version[0]}.{version[1]}.{version[2]}.{version[3]}"
def get_process_list():
h_process_snap = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0)
if h_process_snap == ctypes.wintypes.HANDLE(-1).value:
print("Failed to create snapshot")
return []
pe32 = PROCESSENTRY32()
pe32.dwSize = ctypes.sizeof(PROCESSENTRY32)
process_list = []
if not Process32First(h_process_snap, ctypes.byref(pe32)):
print("Failed to get first process")
CloseHandle(h_process_snap)
return []
while True:
# process_path = get_process_exe_path(pe32.th32ProcessID)
process_list.append((pe32.th32ProcessID, pe32.szExeFile.decode('utf-8', errors='ignore')))
if not Process32Next(h_process_snap, ctypes.byref(pe32)):
break
CloseHandle(h_process_snap)
return process_list
if __name__ == "__main__":
processes = get_process_list()
for pid, name in processes:
if name == "WeChat.exe":
# print(f"PID: {pid}, Process Name: {name}, Exe Path: {path}")
# Handle = ctypes.windll.kernel32.OpenProcess(0x1F0FFF, False, pid)
# wechat_base_address = 0
memory_maps = get_memory_maps(pid)
for module in memory_maps:
if module.FileName and 'WeChatWin.dll' in module.FileName:
print(module.BaseAddress)
print(module.FileName)
break
# print(wechat_base_address)
# get_info_with_key(Handle, key_baseaddr, addrLen)

View File

@ -4,6 +4,7 @@
# Description:
# Author: xaoyaoo
# Date: 2023/08/21
# 注:该部分注释为最初学习使用,仅作参考
# 微信数据库采用的加密算法是256位的AES-CBC。数据库的默认的页大小是4096字节即4KB其中每一个页都是被单独加解密的。
# 加密文件的每一个页都有一个随机的初始化向量,它被保存在每一页的末尾。
# 加密文件的每一页都存有着消息认证码算法使用的是HMAC-SHA1安卓数据库使用的是SHA512。它也被保存在每一页的末尾。

View File

@ -10,14 +10,31 @@ import json
import os
import re
import sys
from ctypes import wintypes
import psutil
import pymem
from .utils import get_exe_version, get_exe_bit, verify_key
from .utils import get_process_list, get_memory_maps, get_process_exe_path, get_file_version_info
from .utils import search_memory
ReadProcessMemory = ctypes.windll.kernel32.ReadProcessMemory if sys.platform == "win32" else None
void_p = ctypes.c_void_p
# 定义常量
PROCESS_QUERY_INFORMATION = 0x0400
PROCESS_VM_READ = 0x0010
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
OpenProcess = kernel32.OpenProcess
OpenProcess.restype = wintypes.HANDLE
OpenProcess.argtypes = [wintypes.DWORD, wintypes.BOOL, wintypes.DWORD]
CloseHandle = kernel32.CloseHandle
CloseHandle.restype = wintypes.BOOL
CloseHandle.argtypes = [wintypes.HANDLE]
class BiasAddr:
def __init__(self, account, mobile, name, key, db_path):
@ -61,10 +78,25 @@ class BiasAddr:
return False, "[-] WeChat No Run"
def search_memory_value(self, value: bytes, module_name="WeChatWin.dll"):
# 创建 Pymem 对象
module = pymem.process.module_from_name(self.pm.process_handle, module_name)
ret = self.pm.pattern_scan_module(value, module, return_multiple=True)
ret = ret[-1] - module.lpBaseOfDll if len(ret) > 0 else 0
start_adress = 0x7FFFFFFFFFFFFFFF
end_adress = 0
memory_maps = get_memory_maps(self.pid)
for module in memory_maps:
if module.FileName and module_name in module.FileName:
s = module.BaseAddress
e = module.BaseAddress + module.RegionSize
start_adress = s if s < start_adress else start_adress
end_adress = e if e > end_adress else end_adress
hProcess = OpenProcess(PROCESS_QUERY_INFORMATION | PROCESS_VM_READ, False, self.pid)
ret = search_memory(hProcess, value, max_num=3, start_address=start_adress,
end_address=end_adress)
ret = ret[-1] - start_adress if len(ret) > 0 else 0
# # 创建 Pymem 对象
# module = pymem.process.module_from_name(self.pm.process_handle, module_name)
# ret = self.pm.pattern_scan_module(value, module, return_multiple=True)
# ret = ret[-1] - module.lpBaseOfDll if len(ret) > 0 else 0
return ret
def get_key_bias1(self):
@ -81,7 +113,6 @@ class BiasAddr:
module = pymem.process.module_from_name(self.process_handle, self.module_name)
keyBytes = b'-----BEGIN PUBLIC KEY-----\n...'
publicKeyList = pymem.pattern.pattern_scan_all(self.process_handle, keyBytes, return_multiple=True)
keyaddrs = []
for addr in publicKeyList:
keyBytes = addr.to_bytes(byteLen, byteorder="little", signed=True) # 低位在前

View File

@ -15,7 +15,7 @@ from typing import List
from .decryption import batch_decrypt
from .wx_info import get_core_db
from .utils import wx_core_loger, wx_core_error
from .utils import wx_core_loger, wx_core_error, CORE_DB_TYPE
@wx_core_error
@ -161,8 +161,23 @@ def merge_db(db_paths: List[dict], save_path: str = "merge.db", is_merge_data: b
# 创建包含 NULL 值比较的 UNIQUE 索引
index_name = f"{table}_unique_index"
coalesce_columns = ','.join(f"COALESCE({column}, '')" for column in columns)
sql = f"CREATE UNIQUE INDEX IF NOT EXISTS {index_name} ON {table} ({coalesce_columns})"
out_cursor.execute(sql)
sql = f"CREATE UNIQUE INDEX IF NOT EXISTS {index_name} ON {table} ({coalesce_columns})" # 创建索引
# ****** 该部分代码来源于 https://github.com/xaoyaoo/PyWxDump/issues/176
# 防止数据重复导致索引创建失败
sql_if_exists_index = f"SELECT 1 FROM sqlite_master WHERE type='index' AND name='{index_name}' AND tbl_name='{table}';"
out_cursor.execute(sql_if_exists_index)
ret_if_exists_index = out_cursor.fetchone()
if ret_if_exists_index is None:
# 之前没创建过索引 先执行删除删除相同数据
# DELETE FROM employees WHERE ROWID NOT IN ( SELECT MIN(ROWID) FROM employees GROUP BY name, position);
str_columns = ','.join(columns)
# sql_clear_same = f"DELETE FROM {table} WHERE ROWID NOT IN (SELECT MIN(ROWID) FROM {table} GROUP BY {str_columns});"
sql_clear_same = f'''WITH Ranked AS (SELECT ROWID, ROW_NUMBER() OVER (PARTITION BY {str_columns} ORDER BY ROWID) AS rn FROM {table})
DELETE FROM {table} WHERE ROWID IN (SELECT ROWID FROM Ranked WHERE rn > 1);'''
out_cursor.execute(sql_clear_same)
out_cursor.execute(sql) # 执行创建索引
# 插入sync_log
sql_query_sync_log = f"SELECT src_count FROM sync_log WHERE db_path=? AND tbl_name=?"
@ -393,13 +408,14 @@ def decrypt_merge(wx_path: str, key: str, outpath: str = "",
@wx_core_error
def merge_real_time_db(key, merge_path: str, db_paths: [dict] or dict):
def merge_real_time_db(key, merge_path: str, db_paths: [dict] or dict, real_time_exe_path: str = None):
"""
合并实时数据库消息,暂时只支持64位系统
:param key: 解密密钥
:param merge_path: 合并后的数据库路径
:param db_paths: [dict] or dict eg: {'wxid': 'wxid_***', 'db_type': 'MicroMsg',
'db_path': 'C:\**\wxid_***\Msg\MicroMsg.db', 'wxid_dir': 'C:\***\wxid_***'}
:param merge_path: 合并后的数据库路径
:param real_time_exe_path: 实时数据库合并工具路径
:return:
"""
try:
@ -419,46 +435,45 @@ def merge_real_time_db(key, merge_path: str, db_paths: [dict] or dict):
os.makedirs(merge_path_base)
endbs = []
for db_info in db_paths:
db_path = os.path.abspath(db_info['db_path'])
if not os.path.exists(db_path):
# raise FileNotFoundError("数据库不存在")
continue
if "MSG" not in db_path and "MicroMsg" not in db_path and "MediaMSG" not in db_path:
# raise FileNotFoundError("数据库不是消息数据库") # MicroMsg实时数据库
continue
endbs.append(os.path.abspath(db_path))
endbs = '" "'.join(list(set(endbs)))
# 获取当前文件夹路径
current_path = os.path.dirname(__file__)
real_time_exe_path = os.path.join(current_path, "tools", "realTime.exe")
if not os.path.exists(real_time_exe_path if real_time_exe_path else ""):
current_path = os.path.dirname(__file__) # 获取当前文件夹路径
real_time_exe_path = os.path.join(current_path, "tools", "realTime.exe")
if not os.path.exists(real_time_exe_path):
raise FileNotFoundError("未找到实时数据库合并工具")
real_time_exe_path = os.path.abspath(real_time_exe_path)
# 调用cmd命令
cmd = f'{real_time_exe_path} "{key}" "{merge_path}" "{endbs}"'
# os.system(cmd)
# wx_core_loger.info(f"合并实时数据库命令:{cmd}")
p = subprocess.Popen(cmd, shell=False, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=merge_path_base,
creationflags=subprocess.CREATE_NO_WINDOW)
# p.communicate()
# 查看返回值
out, err = p.communicate()
out, err = p.communicate() # 查看返回值
if out and out.decode("utf-8").find("SUCCESS") >= 0:
wx_core_loger.info(f"合并实时数据库成功{out}")
return True, merge_path
if err:
else:
wx_core_loger.error(f"合并实时数据库失败\n{out}\n{err}")
return False, err
return False, (out, err)
@wx_core_error
def all_merge_real_time_db(key, wx_path, merge_path: str):
def all_merge_real_time_db(key, wx_path, merge_path: str, real_time_exe_path: str = None):
"""
合并所有实时数据库
这是全量合并会有可能产生重复数据需要自行去重
:param key: 解密密钥
:param wx_path: 微信路径
:param wx_path: 微信文件夹路径 egC:\*****\WeChat Files\wxid*******
:param merge_path: 合并后的数据库路径 eg: C:\\*******\\WeChat Files\\wxid_*********\\merge.db
:param real_time_exe_path: 实时数据库合并工具路径
:return:
"""
if not merge_path or not key or not wx_path or not wx_path:
@ -467,12 +482,12 @@ def all_merge_real_time_db(key, wx_path, merge_path: str):
from pywxdump import get_core_db
except ImportError:
return False, "未找到模块 pywxdump"
db_paths = get_core_db(wx_path, ["MediaMSG", "MSG", "MicroMsg"])
db_paths = get_core_db(wx_path, CORE_DB_TYPE)
if not db_paths[0]:
return False, db_paths[1]
db_paths = db_paths[1]
code, ret = merge_real_time_db(key=key, merge_path=merge_path, db_paths=db_paths)
code, ret = merge_real_time_db(key=key, merge_path=merge_path, db_paths=db_paths,
real_time_exe_path=real_time_exe_path)
if code:
return True, merge_path
else:

Binary file not shown.

View File

@ -12,4 +12,5 @@ from .ctypes_utils import get_process_list, get_memory_maps, get_process_exe_pat
from .memory_search import search_memory
from ._loger import wx_core_loger
DB_TYPE_CORE = ["MSG", "MediaMSG", "MicroMsg", "OpenIMContact", "OpenIMMedia", "OpenIMMsg", "Favorite", "PublicMsg"]
CORE_DB_TYPE = ["MicroMsg", "MSG", "MediaMSG", "OpenIMContact", "OpenIMMsg", "PublicMsg", "OpenIMMedia",
"Favorite", "Sns"]

View File

@ -14,7 +14,7 @@ from typing import List, Union
from .utils import verify_key, get_exe_bit, wx_core_error
from .utils import get_process_list, get_memory_maps, get_process_exe_path, get_file_version_info
from .utils import search_memory
from .utils import wx_core_loger, DB_TYPE_CORE
from .utils import wx_core_loger, CORE_DB_TYPE
import ctypes.wintypes as wintypes
# 定义常量
@ -415,15 +415,15 @@ def get_core_db(wx_path: str, db_types: list = None) -> [dict]:
"""
获取聊天消息核心数据库路径
:param wx_path: 微信文件夹路径 egC:\*****\WeChat Files\wxid*******
:param db_types: 数据库类型 eg: DB_TYPE_CORE中选择一个或多个
:param db_types: 数据库类型 eg: CORE_DB_TYPE中选择一个或多个
:return: 返回数据库路径 eg: [{"wxid": wxid, "db_type": db_type, "db_path": db_path, "wxid_dir": wxid_dir}, ...]
"""
if not os.path.exists(wx_path):
return False, f"[-] 目录不存在: {wx_path}"
if not db_types:
db_types = DB_TYPE_CORE
db_types = [dt for dt in db_types if dt in DB_TYPE_CORE]
db_types = CORE_DB_TYPE
db_types = [dt for dt in db_types if dt in CORE_DB_TYPE]
msg_dir = os.path.dirname(wx_path)
my_wxid = os.path.basename(wx_path)
wxdbpaths = get_wx_db(msg_dir=msg_dir, db_types=db_types, wxids=my_wxid)

View File

@ -1,17 +1,18 @@
setuptools
wheel
psutil
pycryptodomex
pywin32
pymem
silk-python
pyaudio
requests
pillow
flask
pyahocorasick
blackboxprotobuf
lz4
blackboxprotobuf
lxml
flask_cors
pandas
dbutils
psutil
pymem
pydantic==2.7.0
fastapi
uvicorn
python-dotenv

View File

@ -20,15 +20,17 @@ install_requires = [
"silk-python",
"pyaudio",
"requests",
"pillow",
"pyahocorasick",
"flask",
"lz4",
"blackboxprotobuf",
"lxml",
"flask_cors",
"pandas",
"dbutils"
"dbutils",
"fastapi",
"uvicorn",
"python-dotenv",
# "pillow",
]
setup(
@ -42,29 +44,29 @@ setup(
url="https://github.com/xaoyaoo/PyWxDump",
license='MIT',
# packages=find_packages(exclude=[]),
packages=['pywxdump', 'pywxdump.ui', 'pywxdump.wx_core', 'pywxdump.wx_core.utils', 'pywxdump.analyzer',
'pywxdump.api', 'pywxdump.db', 'pywxdump.db.utils', 'pywxdump.db.export'],
'pywxdump.api', 'pywxdump.api.export', 'pywxdump.db', 'pywxdump.db.utils'],
package_dir={'pywxdump': 'pywxdump',
'pywxdump.wx_core': 'pywxdump/wx_core',
'pywxdump.wx_core.utils': 'pywxdump/wx_core/utils',
'pywxdump.analyzer': 'pywxdump/analyzer',
'pywxdump.ui': 'pywxdump/ui',
'pywxdump.api': 'pywxdump/api',
'pywxdump.api.export': 'pywxdump/api/export',
'pywxdump.db': 'pywxdump/db',
'pywxdump.db.utils': 'pywxdump/db/utils',
'pywxdump.db.export': 'pywxdump/db/export'
'pywxdump.db.utils': 'pywxdump/db/utils'
},
# include_package_data=True,
package_data={
'pywxdump': ['WX_OFFS.json', 'ui/templates/*', 'ui/web/*', 'ui/web/assets/*', 'wx_core/tools/*',
"ui/export/*", "ui/export/assets/*", "ui/export/assets/css/*", "ui/export/assets/js/*",
'pywxdump': ['WX_OFFS.json', 'ui/web/*', 'ui/web/assets/*', 'wx_core/tools/*',
]
},
classifiers=[
"Programming Language :: Python :: 3",
"Operating System :: OS Independent",
],
python_requires='>=3.6, <4',
python_requires='>=3.8, <4',
install_requires=install_requires,
entry_points={
'console_scripts': [

View File

@ -16,6 +16,7 @@ ma_version = __version__.split(".")[0]
mi_version = __version__.split(".")[1]
pa_version = __version__.split(".")[2]
def image_to_base64(image_path):
with open(image_path, "rb") as image_file:
encoded_string = base64.b64encode(image_file.read())
@ -38,12 +39,10 @@ spec_content = '''
block_cipher = None
a = Analysis(['tmp.py'],
a = Analysis(['wxdump.py'],
pathex=[],
binaries=[],
datas=[(r'{root_path}\\WX_OFFS.json', 'pywxdump'),
(r'{root_path}/ui/templates/chat.html', 'pywxdump/ui/templates'),
(r'{root_path}/ui/templates/index.html', 'pywxdump/ui/templates'),
{datas_741258}
],
hiddenimports={hidden_imports},
@ -131,11 +130,10 @@ VSVersionInfo(
)
"""
# 创建文件夹
os.makedirs("dist", exist_ok=True)
# 将代码写入文件
with open("dist/tmp.py", "w", encoding="utf-8") as f:
with open("dist/wxdump.py", "w", encoding="utf-8") as f:
f.write(code.strip())
current_path = os.path.dirname(os.path.abspath(__file__))
@ -144,7 +142,6 @@ shutil.copy(os.path.join(current_path, "favicon.ico"), "dist/icon.ico") # 复
with open("dist/wxdump_version_info.txt", "w", encoding="utf-8") as f:
f.write(wxdump_version_info.strip())
# 获取安装包的路径
package_path = site.getsitepackages()
if package_path:
@ -154,27 +151,31 @@ if package_path:
require_path = os.path.join(os.path.dirname(current_path), "requirements.txt") # requirements.txt 路径
with open(require_path, "r", encoding="utf-8") as f:
hidden_imports = f.read().splitlines()
hidden_imports = [i.replace('-','_') for i in hidden_imports if i not in ["setuptools", "wheel"]] # 去掉setuptools、wheel
hidden_imports = [i.replace('-', '_').split("=")[0].split("~")[0] for i in hidden_imports if
i and i not in ["setuptools", "wheel"]] # 去掉setuptools、wheel
hidden_imports += ["pywxdump", "pywxdump.db", "pywxdump.db.__init__.utils"]
# 获取 ui 文件夹下的所有文件 用于打包
root_path = os.path.join(package_path, 'pywxdump')
datas_741258 = []
for root, dirs, files in os.walk(os.path.join(root_path, "ui")):
for root, dirs, files in os.walk(root_path):
for file in files:
file_path = os.path.join(root, file)
datas_741258.append(f'''(r'{file_path}', r'{os.path.dirname(file_path.replace(package_path, "")[1:])}' )''')
if "__pycache__" in file_path:
continue
datas_741258.append(f'''(r'{file_path}', r'{os.path.dirname(file_path.replace(package_path, "")[1:])}')''')
datas_741258 = ",\n".join(datas_741258)
# 获取 wx_core/tools 文件夹下的所有文件 用于打包
for root, dirs, files in os.walk(os.path.join(root_path, "wx_core", "tools")):
for file in files:
file_path = os.path.join(root, file)
datas_741258 += f''',\n(r'{file_path}', r'{os.path.dirname(file_path.replace(package_path, "")[1:])}' )'''
# # 获取 wx_core/tools 文件夹下的所有文件 用于打包
# for root, dirs, files in os.walk(os.path.join(root_path, "wx_core", "tools")):
# for file in files:
# file_path = os.path.join(root, file)
# datas_741258 += f''',\n(r'{file_path}', r'{os.path.dirname(file_path.replace(package_path, "")[1:])}' )'''
# print(datas_741258)
# 生成 spec 文件
spec_content = spec_content.format(root_path=root_path, hidden_imports=hidden_imports, datas_741258=datas_741258, version=__version__)
spec_content = spec_content.format(root_path=root_path, hidden_imports=hidden_imports, datas_741258=datas_741258,
version=__version__)
spec_file = os.path.join("dist", "pywxdump.spec")
with open(spec_file, 'w', encoding="utf-8") as f:
f.write(spec_content.strip())

View File

@ -11,6 +11,7 @@ import time
def custom_sort_key(tag):
tag = tag.split(',')[0]
if tag == 'python':
return "000.000.000"
elif tag == 'HEAD':
@ -68,6 +69,7 @@ log = log.replace("(HEAD -> master)", "")
log = log.replace("(HEAD -> master, origin/master, origin/HEAD)", "")
log = log.replace("(origin/master, origin/HEAD)", "")
log = log.replace("HEAD -> master, ", "").replace(", origin/master, origin/HEAD", "")
log = log.replace("(backup/master)", "")
# 按照tag分割
log = log.split("(tag: ")

View File

@ -6,7 +6,6 @@
# Date: 2024/07/02
# -------------------------------------------------------------------------------
import os
import sys
import time
# 获取当前文件所在目录

View File

@ -5,10 +5,7 @@
# Author: xaoyaoo
# Date: 2023/10/15
# -------------------------------------------------------------------------------
import pywxdump
from pywxdump import WX_OFFS_PATH, WX_OFFS
from pywxdump import BiasAddr
from pywxdump.wx_info import read_info
mobile = '13800138000'
name = '张三'
@ -18,4 +15,3 @@ db_path = None # "xxxxxx"
vlp = None # WX_OFFS_PATH
# 调用 run 函数,并传入参数
rdata = BiasAddr(account, mobile, name, key, db_path).run(True, vlp)

View File

@ -5,33 +5,9 @@
# Author: xaoyaoo
# Date: 2023/11/15
# -------------------------------------------------------------------------------
try:
from flask import Flask, request, jsonify, render_template, g
import logging
from pywxdump.show_chat.main_window import app_show_chat, get_user_list
except Exception as e:
print(e)
print("[-] 请安装flask( pip install flask )")
assert "[-] 请安装flask( pip install flask )"
app = Flask(__name__, template_folder='./show_chat/templates')
app.logger.setLevel(logging.ERROR)
from pywxdump import start_server
msg_path = r"xxxxxx"
micro_path = r"xxxxxx"
media_path = r"xxxxxx"
filestorage_path = r"xxxxxx"
merge_path = r"D:\****.db"
@app.before_request
def before_request():
g.MSG_ALL_db_path = msg_path
g.MicroMsg_db_path = micro_path
g.MediaMSG_all_db_path = media_path
g.FileStorage_path = filestorage_path
g.USER_LIST = get_user_list(msg_path, micro_path)
app.register_blueprint(app_show_chat)
print("[+] 请使用浏览器访问 http://127.0.0.1:5000/ 查看聊天记录")
app.run(debug=False)
start_server(merge_path=merge_path)

View File

@ -5,8 +5,6 @@
# Author: xaoyaoo
# Date: 2023/11/15
# -------------------------------------------------------------------------------
from pywxdump import WX_OFFS_PATH, WX_OFFS
from pywxdump import batch_decrypt
key = "xxxxxx" # 解密密钥

View File

@ -5,10 +5,9 @@
# Author: xaoyaoo
# Date: 2023/10/21
# -------------------------------------------------------------------------------
from pywxdump.wx_info import read_info
from pywxdump import get_wx_info, WX_OFFS
from pywxdump import WX_OFFS_PATH, WX_OFFS
def test_read_info():
result = read_info(WX_OFFS, is_logging=True) # 读取微信信息
assert result is not None
result = get_wx_info(WX_OFFS, is_logging=True) # 读取微信信息
assert result is not None