Compare commits
172 Commits
Author | SHA1 | Date | |
---|---|---|---|
![]() |
e85f9f2882 | ||
![]() |
1f627408da | ||
![]() |
8330cb974a | ||
![]() |
8b652d2d07 | ||
![]() |
e3817b2c0e | ||
![]() |
4a63d6fa3e | ||
![]() |
79d8af07ef | ||
![]() |
0134299066 | ||
![]() |
79ae0b0edb | ||
![]() |
78265602bb | ||
![]() |
7a0c7f384a | ||
![]() |
42679aa5c6 | ||
![]() |
881b0e539e | ||
![]() |
36a9e4f1aa | ||
![]() |
e2f193c616 | ||
![]() |
c03fd47637 | ||
![]() |
ef32e935b8 | ||
![]() |
983610f848 | ||
![]() |
0ac4c9cfbb | ||
![]() |
fcab5ad5e1 | ||
![]() |
6a3d0797fb | ||
![]() |
ee007f5ab1 | ||
![]() |
6d8ff36164 | ||
![]() |
654ccdd177 | ||
![]() |
48bdf80ac4 | ||
![]() |
e0d0703337 | ||
![]() |
4c37b577a6 | ||
![]() |
001364b3c6 | ||
![]() |
9acdb94c07 | ||
![]() |
c870b04dd9 | ||
![]() |
82bf28e591 | ||
![]() |
235a35cebd | ||
![]() |
5cd4f10328 | ||
![]() |
cbebf018d4 | ||
![]() |
4079545ec2 | ||
![]() |
2acc3fb621 | ||
![]() |
d587034d3d | ||
![]() |
f07e65768c | ||
![]() |
51d445891f | ||
![]() |
b9bd48c3b7 | ||
![]() |
91c36ca300 | ||
![]() |
e0b891d698 | ||
![]() |
4a61cda9fa | ||
![]() |
5a479ba514 | ||
![]() |
31e69a6fb5 | ||
![]() |
be3ba1a712 | ||
![]() |
b95fb68cd9 | ||
![]() |
72cb404e35 | ||
![]() |
65d824cf42 | ||
![]() |
a385434c12 | ||
![]() |
d35edd0c6c | ||
![]() |
75d9081217 | ||
![]() |
40a3b60c25 | ||
![]() |
4decd8c6e7 | ||
![]() |
bc53238ade | ||
![]() |
d0e004deb4 | ||
![]() |
caade0cb20 | ||
![]() |
9e3c7309c0 | ||
![]() |
3f2b341210 | ||
![]() |
0314cf4107 | ||
![]() |
f698e4d348 | ||
![]() |
e17888cd80 | ||
![]() |
e75465d93c | ||
![]() |
91b7aa34cb | ||
![]() |
5307297144 | ||
![]() |
c5b9c009df | ||
![]() |
644537b9b6 | ||
![]() |
b7d4841a74 | ||
![]() |
d379e4e953 | ||
![]() |
cf92003e62 | ||
![]() |
bbee43cc9f | ||
![]() |
3cc599c231 | ||
![]() |
1002c2368a | ||
![]() |
ca6c6a022a | ||
![]() |
60ae50eeac | ||
![]() |
00ecf3f458 | ||
![]() |
c4ec6386c7 | ||
![]() |
461e922dcf | ||
![]() |
1ec4bf5965 | ||
![]() |
da47822c92 | ||
![]() |
8887b381b9 | ||
![]() |
4da72a8522 | ||
![]() |
a61a3e7dfe | ||
![]() |
b3dd6a0715 | ||
![]() |
82363ef384 | ||
![]() |
799b28a668 | ||
![]() |
b799cb1384 | ||
![]() |
d917f3f581 | ||
![]() |
b4cc73e495 | ||
![]() |
dab025bb7b | ||
![]() |
0c56e20c9d | ||
![]() |
9f1a7d3899 | ||
![]() |
0f0bc5b19c | ||
![]() |
154b7c5286 | ||
![]() |
08bb49dc9d | ||
![]() |
3d9624f321 | ||
![]() |
09d436a83c | ||
![]() |
e771ec1c32 | ||
![]() |
a4fb7508b3 | ||
![]() |
a1da964849 | ||
![]() |
d51c662659 | ||
![]() |
5b3f26c40c | ||
![]() |
85362627ea | ||
![]() |
df7eecdaa4 | ||
![]() |
0c727dc34f | ||
![]() |
33da8c4b78 | ||
![]() |
3bde268c55 | ||
![]() |
b5fee91e28 | ||
![]() |
e5a0754e7f | ||
![]() |
a2ffebf49e | ||
![]() |
0b937a2f11 | ||
![]() |
81832a2a99 | ||
![]() |
36fcb2ac44 | ||
![]() |
37293a5af2 | ||
![]() |
3ea91fb962 | ||
![]() |
2539b97fb9 | ||
![]() |
d0e75a9f15 | ||
![]() |
bd4a880d7a | ||
![]() |
8b21c1c6b0 | ||
![]() |
ccbcf5878c | ||
![]() |
1922c1da39 | ||
![]() |
4fe4bfd47c | ||
![]() |
e83f58163b | ||
![]() |
7b3ad34f81 | ||
![]() |
7eef77f07a | ||
![]() |
99c273ea11 | ||
![]() |
3a6f070410 | ||
![]() |
0e675f6629 | ||
![]() |
bc3c9baa99 | ||
![]() |
8727c28459 | ||
![]() |
3470b6b71c | ||
![]() |
ddbd0dba92 | ||
![]() |
56d8f9b4dd | ||
![]() |
df409fa39b | ||
![]() |
647f2dc2cc | ||
![]() |
c50a31f814 | ||
![]() |
8ae6dfdbe1 | ||
![]() |
6c1cca2966 | ||
![]() |
c1b638a87b | ||
![]() |
ff8c54b55c | ||
![]() |
9aa3601973 | ||
![]() |
5a96564828 | ||
![]() |
0c020ec602 | ||
![]() |
af6e866a18 | ||
![]() |
49db68bb8e | ||
![]() |
48f977736d | ||
![]() |
be4075df80 | ||
![]() |
472611153a | ||
![]() |
9a74e418ca | ||
![]() |
570e498057 | ||
![]() |
86886f50f2 | ||
![]() |
ee074a568a | ||
![]() |
11921c1381 | ||
![]() |
7f316296b0 | ||
![]() |
fd642b4de0 | ||
![]() |
17c9bacc73 | ||
![]() |
bbce64e111 | ||
![]() |
02d4eb49d7 | ||
![]() |
6359464184 | ||
![]() |
83b794676c | ||
![]() |
a3593865a1 | ||
![]() |
a4ba8fa50d | ||
![]() |
5d18f4193c | ||
![]() |
5ff8e6d646 | ||
![]() |
7f978824cd | ||
![]() |
5630ca4e77 | ||
![]() |
48cbc80d84 | ||
![]() |
187b9cce41 | ||
![]() |
535a767375 | ||
![]() |
102c20a926 | ||
![]() |
c8ee2ea8d1 | ||
![]() |
fbfa78788f |
42
.github/workflows/auto-sync-gitee.yml
vendored
Normal file
42
.github/workflows/auto-sync-gitee.yml
vendored
Normal 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
4
.gitignore
vendored
@ -26,4 +26,6 @@ node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
dist-ssr
|
||||
*.local
|
||||
*.local
|
||||
/pywxdump/ui/web/*
|
||||
/pywxdump/ui/web/assets/*
|
||||
|
57
README.md
57
README.md
@ -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. 使用目的
|
||||
[](https://github.com/xaoyaoo/PyWxDump/graphs/contributors)
|
||||
|
||||
* 本项目仅供学习交流使用,**请勿用于非法用途**,**请勿用于非法用途**,**请勿用于非法用途**,否则后果自负。
|
||||
* 用户理解并同意,任何违反法律法规、侵犯他人合法权益的行为,均与本项目及其开发者无关,后果由用户自行承担。
|
||||
UI CONTRIBUTORS:
|
||||
|
||||
### 2. 使用期限
|
||||
|
||||
* 您应该在下载保存,编译使用本项目的24小时内,删除本项目的源代码和(编译出的)程序;超出此期限的任何使用行为,一概与本项目及其开发者无关。
|
||||
|
||||
### 3. 操作规范
|
||||
|
||||
* 本项目仅允许在授权情况下对数据库进行备份与查看,严禁用于非法目的,否则自行承担所有相关责任;用户如因违反此规定而引发的任何法律责任,将由用户自行承担,与本项目及其开发者无关。
|
||||
* 严禁用于窃取他人隐私,严禁用于窃取他人隐私,严禁用于窃取他人隐私,否则自行承担所有相关责任。
|
||||
* 严禁进行二次开发,严禁进行二次开发,严禁进行二次开发,否则自行承担所有相关责任。
|
||||
|
||||
### 4. 免责声明接受
|
||||
|
||||
* 下载、保存、进一步浏览源代码或者下载安装、编译使用本程序,表示你同意本警告,并承诺遵守它;
|
||||
|
||||
### 5. 禁止用于非法测试或渗透
|
||||
|
||||
* 禁止利用本项目的相关技术从事非法测试或渗透,禁止利用本项目的相关代码或相关技术从事任何非法工作,如因此产生的一切不良后果与本项目及其开发者无关。
|
||||
* 任何因此产生的不良后果,包括但不限于数据泄露、系统瘫痪、侵犯隐私等,均与本项目及其开发者无关,责任由用户自行承担。
|
||||
|
||||
### 6. 免责声明修改
|
||||
|
||||
* 本免责声明可能根据项目运行情况和法律法规的变化进行修改和调整。用户应定期查阅本页面以获取最新版本的免责声明,使用本项目时应遵守最新版本的免责声明。
|
||||
|
||||
### 7. 其他
|
||||
|
||||
* 除本免责声明规定外,用户在使用本项目过程中应遵守相关的法律法规和道德规范。对于因用户违反相关规定而引发的任何纠纷或损失,本项目及其开发者不承担任何责任。
|
||||
|
||||
* 请用户慎重阅读并理解本免责声明的所有内容,确保在使用本项目时严格遵守相关规定。
|
||||
|
||||
# Ⅴ. Acknowledgments
|
||||
|
||||
[](https://github.com/xaoyaoo/PyWxDump/graphs/contributors)[](https://github.com/xaoyaoo/wxdump_web/graphs/contributors)
|
||||
[](https://github.com/xaoyaoo/wxdump_web/graphs/contributors)
|
||||
|
||||
otherContributors:
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
## 注:本方法仅用于提供`pywxdump`的基址获取方式的原理。如果需要快捷获取基址,请执行`wxdump bias` + 各种参数。详细见命令说明(或者可以使用图形界面,启动wxdump.exe,选择实用工具,即可看到偏移获取,输入参数即可)
|
||||
|
||||
### 如何通过CE附加进程
|
||||
|
||||
1. 打开CE >> 选择左上角放大镜按钮 >> 选择微信进程 >> 选择附加到进程
|
||||
|
216
doc/CHANGELOG.md
216
doc/CHANGELOG.md
@ -1,6 +1,219 @@
|
||||
## v3.1.8.(待发布)
|
||||
## 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
|
||||
|
||||
- UPDATE CHANGELOG.md
|
||||
- fix 部分api请求失败情况下,不会噶了
|
||||
|
||||
## v3.1.11
|
||||
|
||||
- fix
|
||||
|
||||
## v3.1.10
|
||||
|
||||
- 增加了一些数据显示
|
||||
- 增加聊天情况热力图
|
||||
- UPDATE CHANGELOG.md
|
||||
|
||||
## v3.1.9
|
||||
|
||||
- UPDATE CHANGELOG.md
|
||||
- fix 更换选项,无法自动启用新的数据库
|
||||
- 调整颜色,但是还是很丑,打算添加自定义颜色进去
|
||||
|
||||
## v3.1.8
|
||||
|
||||
- fix #114
|
||||
- UPDATE CHANGELOG.md
|
||||
|
||||
## v3.1.7
|
||||
|
||||
@ -233,7 +446,6 @@
|
||||
## v3.0.15
|
||||
|
||||
- 增加合并数据库的容错
|
||||
- (backup/master) 增加合并数据库的容错
|
||||
|
||||
## v3.0.14
|
||||
|
||||
|
@ -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. 其他
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
```
|
||||
|
||||
或
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
]
|
||||
}
|
||||
|
@ -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.8"
|
||||
|
||||
__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__"]
|
||||
|
@ -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: 时间间隔 可选值:day、month、year、week
|
||||
"""
|
||||
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)
|
||||
|
@ -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"]
|
||||
|
@ -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
|
@ -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}"
|
||||
|
||||
|
46
pywxdump/api/export/exportHtml.py
Normal file
46
pywxdump/api/export/exportHtml.py
Normal 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
|
@ -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}"
|
||||
|
||||
|
@ -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
|
||||
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
|
||||
@ -123,16 +125,17 @@ def init_key():
|
||||
# 显示堆栈信息
|
||||
ls_loger.error(f"{e}", exc_info=True)
|
||||
db_config = {
|
||||
"key": "merge_all",
|
||||
"key": random_str(16),
|
||||
"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,13 +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")
|
||||
|
||||
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, "test", "last", my_wxid)
|
||||
key = gc.get_conf(my_wxid, "key")
|
||||
db_config = {
|
||||
"key": random_str(16),
|
||||
"type": "sqlite",
|
||||
"path": merge_path
|
||||
}
|
||||
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,
|
||||
@ -185,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:
|
||||
@ -211,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():
|
||||
"""
|
||||
@ -219,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()
|
||||
@ -244,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)
|
||||
|
@ -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)
|
||||
# 合并两个字典,相同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:
|
||||
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,57 +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)
|
||||
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")
|
||||
date_count = DBHandler(db_config).get_date_count(wxid=word, start_time=start_time, end_time=end_time)
|
||||
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():
|
||||
"""
|
||||
@ -525,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():
|
||||
"""
|
||||
@ -535,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():
|
||||
"""
|
||||
@ -552,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')
|
||||
|
@ -40,9 +40,7 @@ def ReJson(code: int, body: [dict, list] = None, msg: str = None, error: str = N
|
||||
rjson['msg'] = msg
|
||||
if code != 0:
|
||||
stack = traceback.extract_stack()
|
||||
project_stack = [frame for frame in stack if "pywxdump" in frame.filename.lower() and
|
||||
any(keyword in frame.filename for keyword in
|
||||
["api", "db", "wx_core", "analyzer", "ui", "cli", "server"])]
|
||||
project_stack = [frame for frame in stack]
|
||||
# 格式化调用栈信息
|
||||
formatted_stack = ''.join(traceback.format_list(project_stack))
|
||||
# stack_trace = ''.join(traceback.format_stack())
|
||||
|
@ -8,14 +8,131 @@
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import string
|
||||
import traceback
|
||||
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):
|
||||
@ -81,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):
|
||||
"""
|
||||
校验文件名是否合法
|
||||
@ -104,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]
|
||||
@ -123,3 +264,7 @@ def gen_base64(path):
|
||||
|
||||
base64_encoded_js = base64.b64encode(js_code).decode('utf-8')
|
||||
return start_str + base64_encoded_js
|
||||
|
||||
|
||||
def random_str(num=16):
|
||||
return ''.join(random.sample(string.ascii_letters + string.digits, num))
|
||||
|
@ -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():
|
||||
|
@ -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,40 +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()
|
||||
|
||||
# print(self.MSG_exist, self.Micro_exist, self.Media_exist, self.OpenIMContact_exist, self.PublicMsg_exist,
|
||||
# self.OpenIMMedia_exist, self.Favorite_exist)
|
||||
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"]
|
||||
|
@ -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")
|
||||
|
@ -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,127 +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):
|
||||
"""
|
||||
获取每日聊天记录数量,包括发送者数量、接收者数量和总数。
|
||||
"""
|
||||
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 either start_time or end_time is not an integer, set both to 0
|
||||
if not (isinstance(start_time, int) and isinstance(end_time, int)):
|
||||
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 = ("SELECT strftime('%Y-%m-%d', 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 either start_time or end_time is not an integer, set both to 0
|
||||
if not (isinstance(start_time, int) and isinstance(end_time, int)):
|
||||
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):
|
||||
|
@ -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:
|
||||
|
@ -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,10 +61,13 @@ class MicroHandler(DatabaseBase):
|
||||
:param id_is_key: id_is_key: True: id作为key,False: name作为key
|
||||
: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:
|
||||
@ -76,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, "
|
||||
@ -88,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
|
||||
@ -122,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 "
|
||||
@ -130,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 []
|
||||
@ -157,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,"
|
||||
@ -184,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
|
||||
@ -203,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
|
||||
@ -218,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, "
|
||||
@ -228,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
|
||||
@ -245,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():
|
||||
@ -254,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
|
||||
|
@ -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
|
||||
|
||||
|
||||
|
@ -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:
|
||||
|
@ -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
55
pywxdump/db/dbSns.py
Normal 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
|
@ -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,6 +79,7 @@ class DatabaseSingletonBase:
|
||||
|
||||
class DatabaseBase(DatabaseSingletonBase):
|
||||
_class_name = "DatabaseBase"
|
||||
existed_tables = []
|
||||
|
||||
def __init__(self, db_config):
|
||||
"""
|
||||
@ -90,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):
|
||||
"""
|
||||
@ -124,19 +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] # 将查询结果转换为列表
|
||||
# 检查所有必需的表是否都在现有表中
|
||||
return all(table in existing_tables for table in required_tables)
|
||||
|
||||
def close(self):
|
||||
self.pool.close()
|
||||
db_loger.info(f"关闭数据库 - {self.config}")
|
||||
|
@ -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"]
|
||||
|
@ -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
|
||||
|
@ -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)
|
@ -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>
|
@ -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>
|
||||
<a id="next_page" class="" href="#">下一页</a>
|
||||
|
||||
|
||||
<input id="ipt_go" type="number" min="1" max="1000"
|
||||
style="width: 80px; margin-right: 10px;"/>/<a id="all_pages"></a>
|
||||
<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>
|
@ -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
|
||||
|
@ -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)
|
@ -4,6 +4,7 @@
|
||||
# Description:
|
||||
# Author: xaoyaoo
|
||||
# Date: 2023/08/21
|
||||
# 注:该部分注释为最初学习使用,仅作参考
|
||||
# 微信数据库采用的加密算法是256位的AES-CBC。数据库的默认的页大小是4096字节即4KB,其中每一个页都是被单独加解密的。
|
||||
# 加密文件的每一个页都有一个随机的初始化向量,它被保存在每一页的末尾。
|
||||
# 加密文件的每一页都存有着消息认证码,算法使用的是HMAC-SHA1(安卓数据库使用的是SHA512)。它也被保存在每一页的末尾。
|
||||
|
@ -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) # 低位在前
|
||||
|
@ -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: 微信文件夹路径 eg:C:\*****\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.
@ -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"]
|
||||
|
@ -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
|
||||
|
||||
# 定义常量
|
||||
@ -389,7 +389,7 @@ def get_wx_db(msg_dir: str = None,
|
||||
db_types = None
|
||||
|
||||
wxid_dirs = {} # wx用户目录
|
||||
if "All Users" in os.listdir(msg_dir) or "Applet" in os.listdir(msg_dir) or "WMPF" in os.listdir(msg_dir):
|
||||
if wxids or "All Users" in os.listdir(msg_dir) or "Applet" in os.listdir(msg_dir) or "WMPF" in os.listdir(msg_dir):
|
||||
for sub_dir in os.listdir(msg_dir):
|
||||
if os.path.isdir(os.path.join(msg_dir, sub_dir)) and sub_dir not in ["All Users", "Applet", "WMPF"]:
|
||||
wxid_dirs[os.path.basename(sub_dir)] = os.path.join(msg_dir, sub_dir)
|
||||
@ -415,15 +415,15 @@ def get_core_db(wx_path: str, db_types: list = None) -> [dict]:
|
||||
"""
|
||||
获取聊天消息核心数据库路径
|
||||
:param wx_path: 微信文件夹路径 eg:C:\*****\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)
|
||||
|
@ -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
|
26
setup.py
26
setup.py
@ -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': [
|
||||
|
@ -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())
|
||||
|
@ -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: ")
|
||||
|
@ -6,7 +6,6 @@
|
||||
# Date: 2024/07/02
|
||||
# -------------------------------------------------------------------------------
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
|
||||
# 获取当前文件所在目录
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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" # 解密密钥
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user