From 6535ed011c90f4ce5daea6986d420617b14a70f2 Mon Sep 17 00:00:00 2001
From: SiYuan <863909694@qq.com>
Date: Fri, 28 Mar 2025 21:29:18 +0800
Subject: [PATCH] =?UTF-8?q?=E9=87=8D=E5=86=99=E6=9E=B6=E6=9E=84=EF=BC=8C?=
=?UTF-8?q?=E6=94=AF=E6=8C=81=E5=BE=AE=E4=BF=A14.0?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.gitignore | 3 +
LICENSE | 695 +-
app/DataBase/__init__.py | 40 -
app/DataBase/hard_link.py | 297 -
app/DataBase/media_msg.py | 145 -
app/DataBase/merge.py | 103 -
app/DataBase/micro_msg.py | 152 -
app/DataBase/misc.py | 78 -
app/DataBase/msg.py | 894 -
app/DataBase/package_msg.py | 184 -
app/__init__.py | 9 -
app/analysis/analysis.py | 535 -
app/components/Button_Contact.py | 103 -
app/components/CAvatar.py | 303 -
app/components/QCursorGif.py | 71 -
app/components/__init__.py | 2 -
app/components/bubble_message.py | 301 -
app/components/calendar_dialog.py | 78 -
app/components/contact_info_ui.py | 104 -
app/components/export_contact_item.py | 127 -
app/components/prompt_bar.py | 39 -
app/components/scroll_bar.py | 48 -
app/config.py | 35 -
app/data/__init__.py | 9 -
app/decrypt/decrypt.py | 208 -
app/decrypt/get_wx_info.py | 468 -
app/decrypt/version_list.json | 1011 -
app/log/__init__.py | 3 -
app/log/exception_handling.py | 75 -
app/person.py | 152 -
app/resources/__init__.py | 0
app/resources/data/file.png | Bin 568 -> 0 bytes
app/resources/data/icons/csv.png | Bin 6549 -> 0 bytes
app/resources/data/icons/excel.png | Bin 6094 -> 0 bytes
app/resources/data/icons/file.png | Bin 568 -> 0 bytes
app/resources/data/icons/pause.png | Bin 1684 -> 0 bytes
app/resources/data/icons/pdf.png | Bin 5474 -> 0 bytes
app/resources/data/icons/phone.png | Bin 12454 -> 0 bytes
app/resources/data/icons/play.png | Bin 1875 -> 0 bytes
app/resources/data/icons/ppt.png | Bin 5126 -> 0 bytes
app/resources/data/icons/transfer1.png | Bin 7734 -> 0 bytes
app/resources/data/icons/transfer2.png | Bin 6570 -> 0 bytes
app/resources/data/icons/transfer3.png | Bin 1398 -> 0 bytes
app/resources/data/icons/txt.png | Bin 4937 -> 0 bytes
app/resources/data/icons/video.png | Bin 9483 -> 0 bytes
app/resources/data/icons/word.png | Bin 6367 -> 0 bytes
app/resources/data/icons/zip.png | Bin 6641 -> 0 bytes
app/resources/data/stopwords.txt | 2540 ---
app/resources/data/template.html | 1821 --
app/resources/icons/404.png | Bin 1994 -> 0 bytes
app/resources/icons/404.svg | 13 -
app/resources/icons/Cursors/0.png | Bin 1382 -> 0 bytes
app/resources/icons/Cursors/1.png | Bin 1402 -> 0 bytes
app/resources/icons/Cursors/2.png | Bin 1567 -> 0 bytes
app/resources/icons/Cursors/3.png | Bin 1541 -> 0 bytes
app/resources/icons/Cursors/4.png | Bin 1567 -> 0 bytes
app/resources/icons/Cursors/5.png | Bin 1597 -> 0 bytes
app/resources/icons/Cursors/6.png | Bin 1615 -> 0 bytes
app/resources/icons/Cursors/7.png | Bin 1644 -> 0 bytes
app/resources/icons/__init__.py | 0
app/resources/icons/analysis.svg | 7 -
app/resources/icons/annual_report.svg | 1 -
app/resources/icons/annual_report0.svg | 12 -
app/resources/icons/annual_report1.svg | 40 -
app/resources/icons/arrow-left.svg | 1 -
app/resources/icons/arrow-right.svg | 1 -
app/resources/icons/back.svg | 8 -
app/resources/icons/chat.svg | 1 -
app/resources/icons/chat0.svg | 10 -
app/resources/icons/clear.svg | 1 -
app/resources/icons/contact.svg | 1 -
app/resources/icons/csv.svg | 11 -
app/resources/icons/decrypt.svg | 1 -
app/resources/icons/default_avatar.svg | 134 -
app/resources/icons/down.svg | 1 -
app/resources/icons/emotion.svg | 11 -
app/resources/icons/folder.svg | 1 -
app/resources/icons/get_wx_info.svg | 1 -
app/resources/icons/help.svg | 1 -
app/resources/icons/home.svg | 1 -
app/resources/icons/home0.svg | 1 -
app/resources/icons/html.svg | 11 -
app/resources/icons/loading.svg | 11 -
app/resources/icons/logo.png | Bin 40265 -> 0 bytes
app/resources/icons/logo.svg | 13 -
app/resources/icons/logo3.0.ico | Bin 456414 -> 0 bytes
app/resources/icons/logo99.png | Bin 8006 -> 0 bytes
app/resources/icons/man.svg | 1 -
app/resources/icons/myinfo.svg | 7 -
app/resources/icons/output.svg | 1 -
app/resources/icons/output0.svg | 9 -
app/resources/icons/resources.qrc | 5 -
app/resources/icons/resources_rc.py | 202 -
app/resources/icons/search.svg | 1 -
app/resources/icons/select.svg | 1 -
app/resources/icons/start.svg | 1 -
app/resources/icons/tool.svg | 1 -
app/resources/icons/tool0.svg | 1 -
app/resources/icons/txt.svg | 1 -
app/resources/icons/unselect.svg | 1 -
app/resources/icons/up.svg | 1 -
app/resources/icons/update.svg | 1 -
app/resources/icons/weixin.png | Bin 74298 -> 0 bytes
app/resources/icons/woman.svg | 1 -
app/resources/icons/word.svg | 18 -
app/resources/icons/关闭.svg | 1 -
app/resources/icons/关闭状态.svg | 1 -
app/resources/icons/按钮_关闭.svg | 8 -
app/resources/icons/按钮_开启.svg | 1 -
app/resources/resource_rc.py | 18359 ----------------
app/ui/Icon.py | 45 -
app/ui/QSS/style.qss | 105 -
app/ui/__init__.py | 0
app/ui/chat/__init__.py | 1 -
app/ui/chat/ai_chat.py | 217 -
app/ui/chat/chatInfoUi.py | 74 -
app/ui/chat/chatUi.py | 66 -
app/ui/chat/chat_info.py | 188 -
app/ui/chat/chat_window.py | 186 -
app/ui/contact/__init__.py | 1 -
app/ui/contact/contactInfo.py | 155 -
app/ui/contact/contactInfoUi.py | 98 -
app/ui/contact/contactUi.py | 66 -
app/ui/contact/contact_window.py | 196 -
app/ui/contact/export/__init__.py | 0
app/ui/contact/export/exportUi.py | 112 -
app/ui/contact/export/export_dialog.py | 229 -
app/ui/contact/userinfo/__init__.py | 9 -
app/ui/contact/userinfo/userinfo.py | 62 -
app/ui/contact/userinfo/userinfoUi.py | 168 -
app/ui/home/__init__.py | 0
app/ui/home/home_window.py | 79 -
app/ui/home/home_windowUi.py | 109 -
app/ui/mainview.py | 524 -
app/ui/mainwindow.py | 191 -
app/ui/menu/__init__.py | 0
app/ui/menu/about_dialog.cp310-win_amd64.pyd | Bin 177664 -> 0 bytes
app/ui/menu/about_dialog.cp311-win_amd64.pyd | Bin 179200 -> 0 bytes
app/ui/menu/about_dialog.cp312-win_amd64.pyd | Bin 178688 -> 0 bytes
app/ui/menu/dialog.py | 61 -
app/ui/menu/export.py | 288 -
app/ui/menu/exportUi.py | 166 -
app/ui/menu/export_time_range.py | 109 -
app/ui/menu/time_range.py | 73 -
app/ui/tool/get_bias_addr/getBiasAddrUi.py | 206 -
app/ui/tool/get_bias_addr/get_bias_addr.py | 146 -
app/ui/tool/pc_decrypt/__init__.py | 3 -
app/ui/tool/pc_decrypt/decryptUi.py | 206 -
app/ui/tool/pc_decrypt/pc_decrypt.py | 340 -
app/ui/tool/setting/setting.py | 208 -
app/ui/tool/setting/settingUi.py | 204 -
app/ui/tool/toolUI.py | 85 -
app/ui/tool/tool_window.py | 111 -
app/ui/update/update.py | 11 -
app/ui/update/updateUi.py | 52 -
app/util/__init__.py | 1 -
app/util/compress_content.py | 333 -
app/util/emoji.py | 318 -
app/util/exporter/__init__.py | 0
app/util/exporter/exporter.py | 176 -
app/util/exporter/exporter_ai_txt.py | 96 -
app/util/exporter/exporter_csv.py | 40 -
app/util/exporter/exporter_docx.py | 380 -
app/util/exporter/exporter_html.py | 523 -
app/util/exporter/exporter_json.py | 193 -
app/util/exporter/exporter_txt.py | 146 -
app/util/exporter/output.py | 466 -
app/util/file.py | 59 -
app/util/image.py | 135 -
app/util/music.py | 55 -
app/util/path.py | 81 -
app/util/protocbuf/__init__.py | 0
app/util/search.py | 14 -
app/web_ui/__init__.py | 0
app/web_ui/templates/charts.html | 155 -
app/web_ui/templates/christmas.html | 383 -
app/web_ui/templates/home.html | 177 -
app/web_ui/templates/index.html | 441 -
app/web_ui/templates/wordcloud.html | 99 -
app/web_ui/web.py | 297 -
doc/开发者手册.md | 82 +-
example/1-decrypt.py | 57 +
example/README.md | 197 +
exporter/__init__.py | 7 +
exporter/config.py | 33 +
exporter/exporter.py | 649 +
exporter/exporter_ai_txt.py | 51 +
exporter/exporter_csv.py | 49 +
exporter/exporter_docx.py | 337 +
exporter/exporter_html.py | 299 +
exporter/exporter_json.py | 305 +
exporter/exporter_markdown.py | 210 +
exporter/exporter_txt.py | 37 +
exporter/exporter_xlsx.py | 526 +
{app/resources/data => exporter}/ffmpeg.exe | Bin
exporter/resources/default_avatar.png | Bin 0 -> 1715 bytes
exporter/resources/emoji/666.png | Bin 0 -> 6623 bytes
exporter/resources/emoji/Emm.png | Bin 0 -> 5395 bytes
exporter/resources/emoji/OK.png | Bin 0 -> 6862 bytes
exporter/resources/emoji/Whimper.png | Bin 0 -> 6229 bytes
exporter/resources/emoji/亲亲.png | Bin 0 -> 5458 bytes
exporter/resources/emoji/便便.png | Bin 0 -> 5693 bytes
exporter/resources/emoji/偷笑.png | Bin 0 -> 6236 bytes
exporter/resources/emoji/傲慢.png | Bin 0 -> 5858 bytes
exporter/resources/emoji/再见.png | Bin 0 -> 5285 bytes
exporter/resources/emoji/凋谢.png | Bin 0 -> 5137 bytes
exporter/resources/emoji/加油.png | Bin 0 -> 6459 bytes
exporter/resources/emoji/加油加油.png | Bin 0 -> 8757 bytes
exporter/resources/emoji/勾引.png | Bin 0 -> 5399 bytes
exporter/resources/emoji/发呆.png | Bin 0 -> 5829 bytes
exporter/resources/emoji/发怒.png | Bin 0 -> 5123 bytes
exporter/resources/emoji/发抖.png | Bin 0 -> 4563 bytes
exporter/resources/emoji/可怜.png | Bin 0 -> 6229 bytes
exporter/resources/emoji/右哼哼.png | Bin 0 -> 4963 bytes
exporter/resources/emoji/叹气.png | Bin 0 -> 5841 bytes
exporter/resources/emoji/吃瓜.png | Bin 0 -> 5850 bytes
exporter/resources/emoji/合十.png | Bin 0 -> 4707 bytes
exporter/resources/emoji/吐.png | Bin 0 -> 5813 bytes
exporter/resources/emoji/吐舌.png | Bin 0 -> 6412 bytes
exporter/resources/emoji/呲牙.png | Bin 0 -> 6278 bytes
exporter/resources/emoji/咒骂.png | Bin 0 -> 6251 bytes
exporter/resources/emoji/咖啡.png | Bin 0 -> 5915 bytes
exporter/resources/emoji/哇.png | Bin 0 -> 6230 bytes
exporter/resources/emoji/啤酒.png | Bin 0 -> 6281 bytes
exporter/resources/emoji/嘘.png | Bin 0 -> 5403 bytes
exporter/resources/emoji/嘴唇.png | Bin 0 -> 4844 bytes
exporter/resources/emoji/嘿哈.png | Bin 0 -> 6338 bytes
exporter/resources/emoji/囧.png | Bin 0 -> 15725 bytes
exporter/resources/emoji/困.png | Bin 0 -> 4831 bytes
exporter/resources/emoji/坏笑.png | Bin 0 -> 5427 bytes
exporter/resources/emoji/大哭.png | Bin 0 -> 5532 bytes
exporter/resources/emoji/天啊.png | Bin 0 -> 5881 bytes
exporter/resources/emoji/太阳.png | Bin 0 -> 6637 bytes
exporter/resources/emoji/失望.png | Bin 0 -> 5723 bytes
exporter/resources/emoji/奸笑.png | Bin 0 -> 6061 bytes
exporter/resources/emoji/好的.png | Bin 0 -> 6479 bytes
exporter/resources/emoji/委屈.png | Bin 0 -> 6177 bytes
exporter/resources/emoji/害羞.png | Bin 0 -> 5561 bytes
exporter/resources/emoji/尴尬.png | Bin 0 -> 4920 bytes
exporter/resources/emoji/庆祝.png | Bin 0 -> 5048 bytes
exporter/resources/emoji/弱.png | Bin 0 -> 4316 bytes
exporter/resources/emoji/强.png | Bin 0 -> 4448 bytes
exporter/resources/emoji/得意.png | Bin 0 -> 5783 bytes
exporter/resources/emoji/微笑.png | Bin 0 -> 5790 bytes
exporter/resources/emoji/心碎.png | Bin 0 -> 5110 bytes
exporter/resources/emoji/快哭了.png | Bin 0 -> 5960 bytes
exporter/resources/emoji/恐惧.png | Bin 0 -> 5653 bytes
exporter/resources/emoji/悠闲.png | Bin 0 -> 5057 bytes
exporter/resources/emoji/惊恐.png | Bin 0 -> 5325 bytes
exporter/resources/emoji/惊讶.png | Bin 0 -> 5354 bytes
exporter/resources/emoji/愉快.png | Bin 0 -> 5163 bytes
exporter/resources/emoji/憨笑.png | Bin 0 -> 6018 bytes
exporter/resources/emoji/打脸.png | Bin 0 -> 5915 bytes
exporter/resources/emoji/抓狂.png | Bin 0 -> 6316 bytes
exporter/resources/emoji/抠鼻.png | Bin 0 -> 5385 bytes
exporter/resources/emoji/抱拳.png | Bin 0 -> 5964 bytes
exporter/resources/emoji/拥抱.png | Bin 0 -> 5018 bytes
exporter/resources/emoji/拳头.png | Bin 0 -> 6776 bytes
exporter/resources/emoji/捂脸.png | Bin 0 -> 6365 bytes
exporter/resources/emoji/握手.png | Bin 0 -> 6325 bytes
exporter/resources/emoji/撇嘴.png | Bin 0 -> 5195 bytes
exporter/resources/emoji/擦汗.png | Bin 0 -> 5603 bytes
exporter/resources/emoji/敲打.png | Bin 0 -> 31889 bytes
exporter/resources/emoji/无语.png | Bin 0 -> 5733 bytes
exporter/resources/emoji/旺柴.png | Bin 0 -> 5690 bytes
exporter/resources/emoji/晕.png | Bin 0 -> 6589 bytes
exporter/resources/emoji/月亮.png | Bin 0 -> 5920 bytes
exporter/resources/emoji/机智.png | Bin 0 -> 6162 bytes
exporter/resources/emoji/汗.png | Bin 0 -> 5498 bytes
exporter/resources/emoji/流泪.png | Bin 0 -> 4738 bytes
exporter/resources/emoji/炸弹.png | Bin 0 -> 5171 bytes
exporter/resources/emoji/烟花.png | Bin 0 -> 6371 bytes
exporter/resources/emoji/爆竹.png | Bin 0 -> 4552 bytes
exporter/resources/emoji/爱心.png | Bin 0 -> 5709 bytes
exporter/resources/emoji/猪头.png | Bin 0 -> 5386 bytes
exporter/resources/emoji/玫瑰.png | Bin 0 -> 4029 bytes
exporter/resources/emoji/生病.png | Bin 0 -> 5904 bytes
exporter/resources/emoji/疑问.png | Bin 0 -> 6362 bytes
exporter/resources/emoji/發.png | Bin 0 -> 5351 bytes
exporter/resources/emoji/白眼.png | Bin 0 -> 5820 bytes
exporter/resources/emoji/皱眉.png | Bin 0 -> 5821 bytes
exporter/resources/emoji/睡.png | Bin 0 -> 5213 bytes
exporter/resources/emoji/破涕为笑.png | Bin 0 -> 6073 bytes
exporter/resources/emoji/礼物.png | Bin 0 -> 6041 bytes
exporter/resources/emoji/社会社会.png | Bin 0 -> 6429 bytes
exporter/resources/emoji/福.png | Bin 0 -> 4750 bytes
exporter/resources/emoji/笑脸.png | Bin 0 -> 5911 bytes
exporter/resources/emoji/红包.png | Bin 0 -> 3199 bytes
exporter/resources/emoji/翻白眼.png | Bin 0 -> 5106 bytes
exporter/resources/emoji/耶.png | Bin 0 -> 6523 bytes
exporter/resources/emoji/胜利.png | Bin 0 -> 6266 bytes
exporter/resources/emoji/脸红.png | Bin 0 -> 5935 bytes
exporter/resources/emoji/色.png | Bin 0 -> 5401 bytes
exporter/resources/emoji/苦涩.png | Bin 0 -> 5438 bytes
exporter/resources/emoji/菜刀.png | Bin 0 -> 3367 bytes
exporter/resources/emoji/蛋糕.png | Bin 0 -> 5853 bytes
exporter/resources/emoji/衰.png | Bin 0 -> 5136 bytes
exporter/resources/emoji/裂开.png | Bin 0 -> 6395 bytes
exporter/resources/emoji/让我看看.png | Bin 0 -> 6692 bytes
exporter/resources/emoji/调皮.png | Bin 0 -> 5042 bytes
exporter/resources/emoji/跳跳.png | Bin 0 -> 4273 bytes
exporter/resources/emoji/转圈.png | Bin 0 -> 4137 bytes
exporter/resources/emoji/鄙视.png | Bin 0 -> 6545 bytes
exporter/resources/emoji/闭嘴.png | Bin 0 -> 5683 bytes
exporter/resources/emoji/阴脸.png | Bin 0 -> 5985 bytes
exporter/resources/emoji/阴险.png | Bin 0 -> 15378 bytes
exporter/resources/emoji/难过.png | Bin 0 -> 5022 bytes
exporter/resources/emoji/難受.png | Bin 0 -> 5438 bytes
exporter/resources/emoji/骷髅.png | Bin 0 -> 6050 bytes
exporter/resources/emoji/鼓掌.png | Bin 0 -> 6203 bytes
exporter/resources/ffmpeg.exe | Bin 0 -> 36560896 bytes
exporter/resources/template.html | 5440 +++++
main.py | 120 -
readme.md | 38 +-
requirements.txt | 36 +-
wxManager/__init__.py | 42 +
wxManager/db_main.py | 254 +
wxManager/db_v3/__init__.py | 13 +
wxManager/db_v3/emotion.py | 135 +
wxManager/db_v3/favorite.py | 37 +
wxManager/db_v3/hard_link_file.py | 89 +
wxManager/db_v3/hard_link_image.py | 157 +
wxManager/db_v3/hard_link_video.py | 119 +
wxManager/db_v3/media_msg.py | 281 +
wxManager/db_v3/micro_msg.py | 204 +
wxManager/db_v3/misc.py | 80 +
wxManager/db_v3/msg.py | 301 +
wxManager/db_v3/open_im_contact.py | 144 +
wxManager/db_v3/open_im_media.py | 47 +
wxManager/db_v3/open_im_msg.py | 147 +
wxManager/db_v3/public_msg.py | 189 +
wxManager/db_v3/sns.py | 210 +
wxManager/db_v4/__init__.py | 19 +
wxManager/db_v4/biz_message.py | 311 +
wxManager/db_v4/contact.py | 152 +
wxManager/db_v4/emotion.py | 55 +
wxManager/db_v4/hardlink.py | 277 +
wxManager/db_v4/head_image.py | 91 +
wxManager/db_v4/media.py | 116 +
wxManager/db_v4/message.py | 316 +
wxManager/db_v4/session.py | 51 +
wxManager/decrypt/__init__.py | 61 +
wxManager/decrypt/common.py | 56 +
wxManager/decrypt/decrypt_dat.py | 307 +
wxManager/decrypt/decrypt_v3.py | 111 +
wxManager/decrypt/decrypt_v4.py | 127 +
{app => wxManager}/decrypt/get_bias_addr.py | 4 +-
wxManager/decrypt/get_wx_info.py | 329 +
.../decrypt}/version_list.json | 63 +
wxManager/decrypt/wx_info_v3.py | 263 +
wxManager/decrypt/wx_info_v4.py | 514 +
wxManager/decrypt/wxinfo.py | 544 +
wxManager/log/__init__.py | 14 +
{app => wxManager}/log/logger.py | 6 +-
wxManager/manager_v3.py | 700 +
wxManager/manager_v4.py | 478 +
wxManager/merge.py | 183 +
wxManager/model/__init__.py | 18 +
wxManager/model/contact.py | 181 +
wxManager/model/db_model.py | 92 +
wxManager/model/message.py | 653 +
wxManager/parser/__init__.py | 13 +
wxManager/parser/audio_parser.py | 39 +
wxManager/parser/emoji_parser.py | 65 +
wxManager/parser/file_parser.py | 61 +
wxManager/parser/link_parser.py | 1232 ++
.../parser/util}/__init__.py | 0
.../parser/util/common.py | 185 +-
.../parser/util/protocbuf}/__init__.py | 0
wxManager/parser/util/protocbuf/contact.proto | 89 +
.../parser/util/protocbuf/contact_pb2.py | 37 +
.../parser/util/protocbuf/emoji_desc.proto | 12 +
.../parser/util/protocbuf/emoji_desc_pb2.py | 27 +
.../parser/util/protocbuf/file_info.proto | 8 +
.../parser/util/protocbuf/file_info_pb2.py | 25 +
.../parser}/util/protocbuf/msg.proto | 0
.../parser}/util/protocbuf/msg_pb2.py | 0
.../util/protocbuf/packed_info_data.proto | 18 +
.../util/protocbuf/packed_info_data_img.proto | 7 +
.../protocbuf/packed_info_data_img_pb2.py | 25 +
.../protocbuf/packed_info_data_merged.proto | 29 +
.../protocbuf/packed_info_data_merged_pb2.py | 33 +
.../util/protocbuf/packed_info_data_pb2.py | 27 +
.../parser}/util/protocbuf/readme.md | 0
.../parser}/util/protocbuf/roomdata.proto | 0
.../parser}/util/protocbuf/roomdata_pb2.py | 0
wxManager/parser/wechat_v3.py | 898 +
wxManager/parser/wechat_v4.py | 947 +
388 files changed, 20483 insertions(+), 39576 deletions(-)
delete mode 100644 app/DataBase/__init__.py
delete mode 100644 app/DataBase/hard_link.py
delete mode 100644 app/DataBase/media_msg.py
delete mode 100644 app/DataBase/merge.py
delete mode 100644 app/DataBase/micro_msg.py
delete mode 100644 app/DataBase/misc.py
delete mode 100644 app/DataBase/msg.py
delete mode 100644 app/DataBase/package_msg.py
delete mode 100644 app/__init__.py
delete mode 100644 app/analysis/analysis.py
delete mode 100644 app/components/Button_Contact.py
delete mode 100644 app/components/CAvatar.py
delete mode 100644 app/components/QCursorGif.py
delete mode 100644 app/components/__init__.py
delete mode 100644 app/components/bubble_message.py
delete mode 100644 app/components/calendar_dialog.py
delete mode 100644 app/components/contact_info_ui.py
delete mode 100644 app/components/export_contact_item.py
delete mode 100644 app/components/prompt_bar.py
delete mode 100644 app/components/scroll_bar.py
delete mode 100644 app/config.py
delete mode 100644 app/data/__init__.py
delete mode 100644 app/decrypt/decrypt.py
delete mode 100644 app/decrypt/get_wx_info.py
delete mode 100644 app/decrypt/version_list.json
delete mode 100644 app/log/__init__.py
delete mode 100644 app/log/exception_handling.py
delete mode 100644 app/person.py
delete mode 100644 app/resources/__init__.py
delete mode 100644 app/resources/data/file.png
delete mode 100644 app/resources/data/icons/csv.png
delete mode 100644 app/resources/data/icons/excel.png
delete mode 100644 app/resources/data/icons/file.png
delete mode 100644 app/resources/data/icons/pause.png
delete mode 100644 app/resources/data/icons/pdf.png
delete mode 100644 app/resources/data/icons/phone.png
delete mode 100644 app/resources/data/icons/play.png
delete mode 100644 app/resources/data/icons/ppt.png
delete mode 100644 app/resources/data/icons/transfer1.png
delete mode 100644 app/resources/data/icons/transfer2.png
delete mode 100644 app/resources/data/icons/transfer3.png
delete mode 100644 app/resources/data/icons/txt.png
delete mode 100644 app/resources/data/icons/video.png
delete mode 100644 app/resources/data/icons/word.png
delete mode 100644 app/resources/data/icons/zip.png
delete mode 100644 app/resources/data/stopwords.txt
delete mode 100644 app/resources/data/template.html
delete mode 100644 app/resources/icons/404.png
delete mode 100644 app/resources/icons/404.svg
delete mode 100644 app/resources/icons/Cursors/0.png
delete mode 100644 app/resources/icons/Cursors/1.png
delete mode 100644 app/resources/icons/Cursors/2.png
delete mode 100644 app/resources/icons/Cursors/3.png
delete mode 100644 app/resources/icons/Cursors/4.png
delete mode 100644 app/resources/icons/Cursors/5.png
delete mode 100644 app/resources/icons/Cursors/6.png
delete mode 100644 app/resources/icons/Cursors/7.png
delete mode 100644 app/resources/icons/__init__.py
delete mode 100644 app/resources/icons/analysis.svg
delete mode 100644 app/resources/icons/annual_report.svg
delete mode 100644 app/resources/icons/annual_report0.svg
delete mode 100644 app/resources/icons/annual_report1.svg
delete mode 100644 app/resources/icons/arrow-left.svg
delete mode 100644 app/resources/icons/arrow-right.svg
delete mode 100644 app/resources/icons/back.svg
delete mode 100644 app/resources/icons/chat.svg
delete mode 100644 app/resources/icons/chat0.svg
delete mode 100644 app/resources/icons/clear.svg
delete mode 100644 app/resources/icons/contact.svg
delete mode 100644 app/resources/icons/csv.svg
delete mode 100644 app/resources/icons/decrypt.svg
delete mode 100644 app/resources/icons/default_avatar.svg
delete mode 100644 app/resources/icons/down.svg
delete mode 100644 app/resources/icons/emotion.svg
delete mode 100644 app/resources/icons/folder.svg
delete mode 100644 app/resources/icons/get_wx_info.svg
delete mode 100644 app/resources/icons/help.svg
delete mode 100644 app/resources/icons/home.svg
delete mode 100644 app/resources/icons/home0.svg
delete mode 100644 app/resources/icons/html.svg
delete mode 100644 app/resources/icons/loading.svg
delete mode 100644 app/resources/icons/logo.png
delete mode 100644 app/resources/icons/logo.svg
delete mode 100644 app/resources/icons/logo3.0.ico
delete mode 100644 app/resources/icons/logo99.png
delete mode 100644 app/resources/icons/man.svg
delete mode 100644 app/resources/icons/myinfo.svg
delete mode 100644 app/resources/icons/output.svg
delete mode 100644 app/resources/icons/output0.svg
delete mode 100644 app/resources/icons/resources.qrc
delete mode 100644 app/resources/icons/resources_rc.py
delete mode 100644 app/resources/icons/search.svg
delete mode 100644 app/resources/icons/select.svg
delete mode 100644 app/resources/icons/start.svg
delete mode 100644 app/resources/icons/tool.svg
delete mode 100644 app/resources/icons/tool0.svg
delete mode 100644 app/resources/icons/txt.svg
delete mode 100644 app/resources/icons/unselect.svg
delete mode 100644 app/resources/icons/up.svg
delete mode 100644 app/resources/icons/update.svg
delete mode 100644 app/resources/icons/weixin.png
delete mode 100644 app/resources/icons/woman.svg
delete mode 100644 app/resources/icons/word.svg
delete mode 100644 app/resources/icons/关闭.svg
delete mode 100644 app/resources/icons/关闭状态.svg
delete mode 100644 app/resources/icons/按钮_关闭.svg
delete mode 100644 app/resources/icons/按钮_开启.svg
delete mode 100644 app/resources/resource_rc.py
delete mode 100644 app/ui/Icon.py
delete mode 100644 app/ui/QSS/style.qss
delete mode 100644 app/ui/__init__.py
delete mode 100644 app/ui/chat/__init__.py
delete mode 100644 app/ui/chat/ai_chat.py
delete mode 100644 app/ui/chat/chatInfoUi.py
delete mode 100644 app/ui/chat/chatUi.py
delete mode 100644 app/ui/chat/chat_info.py
delete mode 100644 app/ui/chat/chat_window.py
delete mode 100644 app/ui/contact/__init__.py
delete mode 100644 app/ui/contact/contactInfo.py
delete mode 100644 app/ui/contact/contactInfoUi.py
delete mode 100644 app/ui/contact/contactUi.py
delete mode 100644 app/ui/contact/contact_window.py
delete mode 100644 app/ui/contact/export/__init__.py
delete mode 100644 app/ui/contact/export/exportUi.py
delete mode 100644 app/ui/contact/export/export_dialog.py
delete mode 100644 app/ui/contact/userinfo/__init__.py
delete mode 100644 app/ui/contact/userinfo/userinfo.py
delete mode 100644 app/ui/contact/userinfo/userinfoUi.py
delete mode 100644 app/ui/home/__init__.py
delete mode 100644 app/ui/home/home_window.py
delete mode 100644 app/ui/home/home_windowUi.py
delete mode 100644 app/ui/mainview.py
delete mode 100644 app/ui/mainwindow.py
delete mode 100644 app/ui/menu/__init__.py
delete mode 100644 app/ui/menu/about_dialog.cp310-win_amd64.pyd
delete mode 100644 app/ui/menu/about_dialog.cp311-win_amd64.pyd
delete mode 100644 app/ui/menu/about_dialog.cp312-win_amd64.pyd
delete mode 100644 app/ui/menu/dialog.py
delete mode 100644 app/ui/menu/export.py
delete mode 100644 app/ui/menu/exportUi.py
delete mode 100644 app/ui/menu/export_time_range.py
delete mode 100644 app/ui/menu/time_range.py
delete mode 100644 app/ui/tool/get_bias_addr/getBiasAddrUi.py
delete mode 100644 app/ui/tool/get_bias_addr/get_bias_addr.py
delete mode 100644 app/ui/tool/pc_decrypt/__init__.py
delete mode 100644 app/ui/tool/pc_decrypt/decryptUi.py
delete mode 100644 app/ui/tool/pc_decrypt/pc_decrypt.py
delete mode 100644 app/ui/tool/setting/setting.py
delete mode 100644 app/ui/tool/setting/settingUi.py
delete mode 100644 app/ui/tool/toolUI.py
delete mode 100644 app/ui/tool/tool_window.py
delete mode 100644 app/ui/update/update.py
delete mode 100644 app/ui/update/updateUi.py
delete mode 100644 app/util/__init__.py
delete mode 100644 app/util/compress_content.py
delete mode 100644 app/util/emoji.py
delete mode 100644 app/util/exporter/__init__.py
delete mode 100644 app/util/exporter/exporter.py
delete mode 100644 app/util/exporter/exporter_ai_txt.py
delete mode 100644 app/util/exporter/exporter_csv.py
delete mode 100644 app/util/exporter/exporter_docx.py
delete mode 100644 app/util/exporter/exporter_html.py
delete mode 100644 app/util/exporter/exporter_json.py
delete mode 100644 app/util/exporter/exporter_txt.py
delete mode 100644 app/util/exporter/output.py
delete mode 100644 app/util/file.py
delete mode 100644 app/util/image.py
delete mode 100644 app/util/music.py
delete mode 100644 app/util/path.py
delete mode 100644 app/util/protocbuf/__init__.py
delete mode 100644 app/util/search.py
delete mode 100644 app/web_ui/__init__.py
delete mode 100644 app/web_ui/templates/charts.html
delete mode 100644 app/web_ui/templates/christmas.html
delete mode 100644 app/web_ui/templates/home.html
delete mode 100644 app/web_ui/templates/index.html
delete mode 100644 app/web_ui/templates/wordcloud.html
delete mode 100644 app/web_ui/web.py
create mode 100644 example/1-decrypt.py
create mode 100644 example/README.md
create mode 100644 exporter/__init__.py
create mode 100644 exporter/config.py
create mode 100644 exporter/exporter.py
create mode 100644 exporter/exporter_ai_txt.py
create mode 100644 exporter/exporter_csv.py
create mode 100644 exporter/exporter_docx.py
create mode 100644 exporter/exporter_html.py
create mode 100644 exporter/exporter_json.py
create mode 100644 exporter/exporter_markdown.py
create mode 100644 exporter/exporter_txt.py
create mode 100644 exporter/exporter_xlsx.py
rename {app/resources/data => exporter}/ffmpeg.exe (100%)
create mode 100644 exporter/resources/default_avatar.png
create mode 100644 exporter/resources/emoji/666.png
create mode 100644 exporter/resources/emoji/Emm.png
create mode 100644 exporter/resources/emoji/OK.png
create mode 100644 exporter/resources/emoji/Whimper.png
create mode 100644 exporter/resources/emoji/亲亲.png
create mode 100644 exporter/resources/emoji/便便.png
create mode 100644 exporter/resources/emoji/偷笑.png
create mode 100644 exporter/resources/emoji/傲慢.png
create mode 100644 exporter/resources/emoji/再见.png
create mode 100644 exporter/resources/emoji/凋谢.png
create mode 100644 exporter/resources/emoji/加油.png
create mode 100644 exporter/resources/emoji/加油加油.png
create mode 100644 exporter/resources/emoji/勾引.png
create mode 100644 exporter/resources/emoji/发呆.png
create mode 100644 exporter/resources/emoji/发怒.png
create mode 100644 exporter/resources/emoji/发抖.png
create mode 100644 exporter/resources/emoji/可怜.png
create mode 100644 exporter/resources/emoji/右哼哼.png
create mode 100644 exporter/resources/emoji/叹气.png
create mode 100644 exporter/resources/emoji/吃瓜.png
create mode 100644 exporter/resources/emoji/合十.png
create mode 100644 exporter/resources/emoji/吐.png
create mode 100644 exporter/resources/emoji/吐舌.png
create mode 100644 exporter/resources/emoji/呲牙.png
create mode 100644 exporter/resources/emoji/咒骂.png
create mode 100644 exporter/resources/emoji/咖啡.png
create mode 100644 exporter/resources/emoji/哇.png
create mode 100644 exporter/resources/emoji/啤酒.png
create mode 100644 exporter/resources/emoji/嘘.png
create mode 100644 exporter/resources/emoji/嘴唇.png
create mode 100644 exporter/resources/emoji/嘿哈.png
create mode 100644 exporter/resources/emoji/囧.png
create mode 100644 exporter/resources/emoji/困.png
create mode 100644 exporter/resources/emoji/坏笑.png
create mode 100644 exporter/resources/emoji/大哭.png
create mode 100644 exporter/resources/emoji/天啊.png
create mode 100644 exporter/resources/emoji/太阳.png
create mode 100644 exporter/resources/emoji/失望.png
create mode 100644 exporter/resources/emoji/奸笑.png
create mode 100644 exporter/resources/emoji/好的.png
create mode 100644 exporter/resources/emoji/委屈.png
create mode 100644 exporter/resources/emoji/害羞.png
create mode 100644 exporter/resources/emoji/尴尬.png
create mode 100644 exporter/resources/emoji/庆祝.png
create mode 100644 exporter/resources/emoji/弱.png
create mode 100644 exporter/resources/emoji/强.png
create mode 100644 exporter/resources/emoji/得意.png
create mode 100644 exporter/resources/emoji/微笑.png
create mode 100644 exporter/resources/emoji/心碎.png
create mode 100644 exporter/resources/emoji/快哭了.png
create mode 100644 exporter/resources/emoji/恐惧.png
create mode 100644 exporter/resources/emoji/悠闲.png
create mode 100644 exporter/resources/emoji/惊恐.png
create mode 100644 exporter/resources/emoji/惊讶.png
create mode 100644 exporter/resources/emoji/愉快.png
create mode 100644 exporter/resources/emoji/憨笑.png
create mode 100644 exporter/resources/emoji/打脸.png
create mode 100644 exporter/resources/emoji/抓狂.png
create mode 100644 exporter/resources/emoji/抠鼻.png
create mode 100644 exporter/resources/emoji/抱拳.png
create mode 100644 exporter/resources/emoji/拥抱.png
create mode 100644 exporter/resources/emoji/拳头.png
create mode 100644 exporter/resources/emoji/捂脸.png
create mode 100644 exporter/resources/emoji/握手.png
create mode 100644 exporter/resources/emoji/撇嘴.png
create mode 100644 exporter/resources/emoji/擦汗.png
create mode 100644 exporter/resources/emoji/敲打.png
create mode 100644 exporter/resources/emoji/无语.png
create mode 100644 exporter/resources/emoji/旺柴.png
create mode 100644 exporter/resources/emoji/晕.png
create mode 100644 exporter/resources/emoji/月亮.png
create mode 100644 exporter/resources/emoji/机智.png
create mode 100644 exporter/resources/emoji/汗.png
create mode 100644 exporter/resources/emoji/流泪.png
create mode 100644 exporter/resources/emoji/炸弹.png
create mode 100644 exporter/resources/emoji/烟花.png
create mode 100644 exporter/resources/emoji/爆竹.png
create mode 100644 exporter/resources/emoji/爱心.png
create mode 100644 exporter/resources/emoji/猪头.png
create mode 100644 exporter/resources/emoji/玫瑰.png
create mode 100644 exporter/resources/emoji/生病.png
create mode 100644 exporter/resources/emoji/疑问.png
create mode 100644 exporter/resources/emoji/發.png
create mode 100644 exporter/resources/emoji/白眼.png
create mode 100644 exporter/resources/emoji/皱眉.png
create mode 100644 exporter/resources/emoji/睡.png
create mode 100644 exporter/resources/emoji/破涕为笑.png
create mode 100644 exporter/resources/emoji/礼物.png
create mode 100644 exporter/resources/emoji/社会社会.png
create mode 100644 exporter/resources/emoji/福.png
create mode 100644 exporter/resources/emoji/笑脸.png
create mode 100644 exporter/resources/emoji/红包.png
create mode 100644 exporter/resources/emoji/翻白眼.png
create mode 100644 exporter/resources/emoji/耶.png
create mode 100644 exporter/resources/emoji/胜利.png
create mode 100644 exporter/resources/emoji/脸红.png
create mode 100644 exporter/resources/emoji/色.png
create mode 100644 exporter/resources/emoji/苦涩.png
create mode 100644 exporter/resources/emoji/菜刀.png
create mode 100644 exporter/resources/emoji/蛋糕.png
create mode 100644 exporter/resources/emoji/衰.png
create mode 100644 exporter/resources/emoji/裂开.png
create mode 100644 exporter/resources/emoji/让我看看.png
create mode 100644 exporter/resources/emoji/调皮.png
create mode 100644 exporter/resources/emoji/跳跳.png
create mode 100644 exporter/resources/emoji/转圈.png
create mode 100644 exporter/resources/emoji/鄙视.png
create mode 100644 exporter/resources/emoji/闭嘴.png
create mode 100644 exporter/resources/emoji/阴脸.png
create mode 100644 exporter/resources/emoji/阴险.png
create mode 100644 exporter/resources/emoji/难过.png
create mode 100644 exporter/resources/emoji/難受.png
create mode 100644 exporter/resources/emoji/骷髅.png
create mode 100644 exporter/resources/emoji/鼓掌.png
create mode 100644 exporter/resources/ffmpeg.exe
create mode 100644 exporter/resources/template.html
delete mode 100644 main.py
create mode 100644 wxManager/__init__.py
create mode 100644 wxManager/db_main.py
create mode 100644 wxManager/db_v3/__init__.py
create mode 100644 wxManager/db_v3/emotion.py
create mode 100644 wxManager/db_v3/favorite.py
create mode 100644 wxManager/db_v3/hard_link_file.py
create mode 100644 wxManager/db_v3/hard_link_image.py
create mode 100644 wxManager/db_v3/hard_link_video.py
create mode 100644 wxManager/db_v3/media_msg.py
create mode 100644 wxManager/db_v3/micro_msg.py
create mode 100644 wxManager/db_v3/misc.py
create mode 100644 wxManager/db_v3/msg.py
create mode 100644 wxManager/db_v3/open_im_contact.py
create mode 100644 wxManager/db_v3/open_im_media.py
create mode 100644 wxManager/db_v3/open_im_msg.py
create mode 100644 wxManager/db_v3/public_msg.py
create mode 100644 wxManager/db_v3/sns.py
create mode 100644 wxManager/db_v4/__init__.py
create mode 100644 wxManager/db_v4/biz_message.py
create mode 100644 wxManager/db_v4/contact.py
create mode 100644 wxManager/db_v4/emotion.py
create mode 100644 wxManager/db_v4/hardlink.py
create mode 100644 wxManager/db_v4/head_image.py
create mode 100644 wxManager/db_v4/media.py
create mode 100644 wxManager/db_v4/message.py
create mode 100644 wxManager/db_v4/session.py
create mode 100644 wxManager/decrypt/__init__.py
create mode 100644 wxManager/decrypt/common.py
create mode 100644 wxManager/decrypt/decrypt_dat.py
create mode 100644 wxManager/decrypt/decrypt_v3.py
create mode 100644 wxManager/decrypt/decrypt_v4.py
rename {app => wxManager}/decrypt/get_bias_addr.py (98%)
create mode 100644 wxManager/decrypt/get_wx_info.py
rename {app/resources/data => wxManager/decrypt}/version_list.json (94%)
create mode 100644 wxManager/decrypt/wx_info_v3.py
create mode 100644 wxManager/decrypt/wx_info_v4.py
create mode 100644 wxManager/decrypt/wxinfo.py
create mode 100644 wxManager/log/__init__.py
rename {app => wxManager}/log/logger.py (89%)
create mode 100644 wxManager/manager_v3.py
create mode 100644 wxManager/manager_v4.py
create mode 100644 wxManager/merge.py
create mode 100644 wxManager/model/__init__.py
create mode 100644 wxManager/model/contact.py
create mode 100644 wxManager/model/db_model.py
create mode 100644 wxManager/model/message.py
create mode 100644 wxManager/parser/__init__.py
create mode 100644 wxManager/parser/audio_parser.py
create mode 100644 wxManager/parser/emoji_parser.py
create mode 100644 wxManager/parser/file_parser.py
create mode 100644 wxManager/parser/link_parser.py
rename {app/ui/tool => wxManager/parser/util}/__init__.py (100%)
rename app/util/region_conversion.py => wxManager/parser/util/common.py (89%)
rename {app/analysis => wxManager/parser/util/protocbuf}/__init__.py (100%)
create mode 100644 wxManager/parser/util/protocbuf/contact.proto
create mode 100644 wxManager/parser/util/protocbuf/contact_pb2.py
create mode 100644 wxManager/parser/util/protocbuf/emoji_desc.proto
create mode 100644 wxManager/parser/util/protocbuf/emoji_desc_pb2.py
create mode 100644 wxManager/parser/util/protocbuf/file_info.proto
create mode 100644 wxManager/parser/util/protocbuf/file_info_pb2.py
rename {app => wxManager/parser}/util/protocbuf/msg.proto (100%)
rename {app => wxManager/parser}/util/protocbuf/msg_pb2.py (100%)
create mode 100644 wxManager/parser/util/protocbuf/packed_info_data.proto
create mode 100644 wxManager/parser/util/protocbuf/packed_info_data_img.proto
create mode 100644 wxManager/parser/util/protocbuf/packed_info_data_img_pb2.py
create mode 100644 wxManager/parser/util/protocbuf/packed_info_data_merged.proto
create mode 100644 wxManager/parser/util/protocbuf/packed_info_data_merged_pb2.py
create mode 100644 wxManager/parser/util/protocbuf/packed_info_data_pb2.py
rename {app => wxManager/parser}/util/protocbuf/readme.md (100%)
rename {app => wxManager/parser}/util/protocbuf/roomdata.proto (100%)
rename {app => wxManager/parser}/util/protocbuf/roomdata_pb2.py (100%)
create mode 100644 wxManager/parser/wechat_v3.py
create mode 100644 wxManager/parser/wechat_v4.py
diff --git a/.gitignore b/.gitignore
index 8d7c170..67f095f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -14,6 +14,9 @@ app/data/emoji
app/DataBase/Msg/*
server
*.db
+*.bin
+*.json
+*.txt
*.ui
*.pyc
*.bat
diff --git a/LICENSE b/LICENSE
index f288702..fe89547 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,674 +1,21 @@
- GNU GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
-
- Copyright (C) 2007 Free Software Foundation, Inc.
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
-
- Preamble
-
- The GNU General Public License is a free, copyleft license for
-software and other kinds of works.
-
- The licenses for most software and other practical works are designed
-to take away your freedom to share and change the works. By contrast,
-the GNU General Public License is intended to guarantee your freedom to
-share and change all versions of a program--to make sure it remains free
-software for all its users. We, the Free Software Foundation, use the
-GNU General Public License for most of our software; it applies also to
-any other work released this way by its authors. You can apply it to
-your programs, too.
-
- When we speak of free software, we are referring to freedom, not
-price. Our General Public Licenses are designed to make sure that you
-have the freedom to distribute copies of free software (and charge for
-them if you wish), that you receive source code or can get it if you
-want it, that you can change the software or use pieces of it in new
-free programs, and that you know you can do these things.
-
- To protect your rights, we need to prevent others from denying you
-these rights or asking you to surrender the rights. Therefore, you have
-certain responsibilities if you distribute copies of the software, or if
-you modify it: responsibilities to respect the freedom of others.
-
- For example, if you distribute copies of such a program, whether
-gratis or for a fee, you must pass on to the recipients the same
-freedoms that you received. You must make sure that they, too, receive
-or can get the source code. And you must show them these terms so they
-know their rights.
-
- Developers that use the GNU GPL protect your rights with two steps:
-(1) assert copyright on the software, and (2) offer you this License
-giving you legal permission to copy, distribute and/or modify it.
-
- For the developers' and authors' protection, the GPL clearly explains
-that there is no warranty for this free software. For both users' and
-authors' sake, the GPL requires that modified versions be marked as
-changed, so that their problems will not be attributed erroneously to
-authors of previous versions.
-
- Some devices are designed to deny users access to install or run
-modified versions of the software inside them, although the manufacturer
-can do so. This is fundamentally incompatible with the aim of
-protecting users' freedom to change the software. The systematic
-pattern of such abuse occurs in the area of products for individuals to
-use, which is precisely where it is most unacceptable. Therefore, we
-have designed this version of the GPL to prohibit the practice for those
-products. If such problems arise substantially in other domains, we
-stand ready to extend this provision to those domains in future versions
-of the GPL, as needed to protect the freedom of users.
-
- Finally, every program is threatened constantly by software patents.
-States should not allow patents to restrict development and use of
-software on general-purpose computers, but in those that do, we wish to
-avoid the special danger that patents applied to a free program could
-make it effectively proprietary. To prevent this, the GPL assures that
-patents cannot be used to render the program non-free.
-
- The precise terms and conditions for copying, distribution and
-modification follow.
-
- TERMS AND CONDITIONS
-
- 0. Definitions.
-
- "This License" refers to version 3 of the GNU General Public License.
-
- "Copyright" also means copyright-like laws that apply to other kinds of
-works, such as semiconductor masks.
-
- "The Program" refers to any copyrightable work licensed under this
-License. Each licensee is addressed as "you". "Licensees" and
-"recipients" may be individuals or organizations.
-
- To "modify" a work means to copy from or adapt all or part of the work
-in a fashion requiring copyright permission, other than the making of an
-exact copy. The resulting work is called a "modified version" of the
-earlier work or a work "based on" the earlier work.
-
- A "covered work" means either the unmodified Program or a work based
-on the Program.
-
- To "propagate" a work means to do anything with it that, without
-permission, would make you directly or secondarily liable for
-infringement under applicable copyright law, except executing it on a
-computer or modifying a private copy. Propagation includes copying,
-distribution (with or without modification), making available to the
-public, and in some countries other activities as well.
-
- To "convey" a work means any kind of propagation that enables other
-parties to make or receive copies. Mere interaction with a user through
-a computer network, with no transfer of a copy, is not conveying.
-
- An interactive user interface displays "Appropriate Legal Notices"
-to the extent that it includes a convenient and prominently visible
-feature that (1) displays an appropriate copyright notice, and (2)
-tells the user that there is no warranty for the work (except to the
-extent that warranties are provided), that licensees may convey the
-work under this License, and how to view a copy of this License. If
-the interface presents a list of user commands or options, such as a
-menu, a prominent item in the list meets this criterion.
-
- 1. Source Code.
-
- The "source code" for a work means the preferred form of the work
-for making modifications to it. "Object code" means any non-source
-form of a work.
-
- A "Standard Interface" means an interface that either is an official
-standard defined by a recognized standards body, or, in the case of
-interfaces specified for a particular programming language, one that
-is widely used among developers working in that language.
-
- The "System Libraries" of an executable work include anything, other
-than the work as a whole, that (a) is included in the normal form of
-packaging a Major Component, but which is not part of that Major
-Component, and (b) serves only to enable use of the work with that
-Major Component, or to implement a Standard Interface for which an
-implementation is available to the public in source code form. A
-"Major Component", in this context, means a major essential component
-(kernel, window system, and so on) of the specific operating system
-(if any) on which the executable work runs, or a compiler used to
-produce the work, or an object code interpreter used to run it.
-
- The "Corresponding Source" for a work in object code form means all
-the source code needed to generate, install, and (for an executable
-work) run the object code and to modify the work, including scripts to
-control those activities. However, it does not include the work's
-System Libraries, or general-purpose tools or generally available free
-programs which are used unmodified in performing those activities but
-which are not part of the work. For example, Corresponding Source
-includes interface definition files associated with source files for
-the work, and the source code for shared libraries and dynamically
-linked subprograms that the work is specifically designed to require,
-such as by intimate data communication or control flow between those
-subprograms and other parts of the work.
-
- The Corresponding Source need not include anything that users
-can regenerate automatically from other parts of the Corresponding
-Source.
-
- The Corresponding Source for a work in source code form is that
-same work.
-
- 2. Basic Permissions.
-
- All rights granted under this License are granted for the term of
-copyright on the Program, and are irrevocable provided the stated
-conditions are met. This License explicitly affirms your unlimited
-permission to run the unmodified Program. The output from running a
-covered work is covered by this License only if the output, given its
-content, constitutes a covered work. This License acknowledges your
-rights of fair use or other equivalent, as provided by copyright law.
-
- You may make, run and propagate covered works that you do not
-convey, without conditions so long as your license otherwise remains
-in force. You may convey covered works to others for the sole purpose
-of having them make modifications exclusively for you, or provide you
-with facilities for running those works, provided that you comply with
-the terms of this License in conveying all material for which you do
-not control copyright. Those thus making or running the covered works
-for you must do so exclusively on your behalf, under your direction
-and control, on terms that prohibit them from making any copies of
-your copyrighted material outside their relationship with you.
-
- Conveying under any other circumstances is permitted solely under
-the conditions stated below. Sublicensing is not allowed; section 10
-makes it unnecessary.
-
- 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
-
- No covered work shall be deemed part of an effective technological
-measure under any applicable law fulfilling obligations under article
-11 of the WIPO copyright treaty adopted on 20 December 1996, or
-similar laws prohibiting or restricting circumvention of such
-measures.
-
- When you convey a covered work, you waive any legal power to forbid
-circumvention of technological measures to the extent such circumvention
-is effected by exercising rights under this License with respect to
-the covered work, and you disclaim any intention to limit operation or
-modification of the work as a means of enforcing, against the work's
-users, your or third parties' legal rights to forbid circumvention of
-technological measures.
-
- 4. Conveying Verbatim Copies.
-
- You may convey verbatim copies of the Program's source code as you
-receive it, in any medium, provided that you conspicuously and
-appropriately publish on each copy an appropriate copyright notice;
-keep intact all notices stating that this License and any
-non-permissive terms added in accord with section 7 apply to the code;
-keep intact all notices of the absence of any warranty; and give all
-recipients a copy of this License along with the Program.
-
- You may charge any price or no price for each copy that you convey,
-and you may offer support or warranty protection for a fee.
-
- 5. Conveying Modified Source Versions.
-
- You may convey a work based on the Program, or the modifications to
-produce it from the Program, in the form of source code under the
-terms of section 4, provided that you also meet all of these conditions:
-
- a) The work must carry prominent notices stating that you modified
- it, and giving a relevant date.
-
- b) The work must carry prominent notices stating that it is
- released under this License and any conditions added under section
- 7. This requirement modifies the requirement in section 4 to
- "keep intact all notices".
-
- c) You must license the entire work, as a whole, under this
- License to anyone who comes into possession of a copy. This
- License will therefore apply, along with any applicable section 7
- additional terms, to the whole of the work, and all its parts,
- regardless of how they are packaged. This License gives no
- permission to license the work in any other way, but it does not
- invalidate such permission if you have separately received it.
-
- d) If the work has interactive user interfaces, each must display
- Appropriate Legal Notices; however, if the Program has interactive
- interfaces that do not display Appropriate Legal Notices, your
- work need not make them do so.
-
- A compilation of a covered work with other separate and independent
-works, which are not by their nature extensions of the covered work,
-and which are not combined with it such as to form a larger program,
-in or on a volume of a storage or distribution medium, is called an
-"aggregate" if the compilation and its resulting copyright are not
-used to limit the access or legal rights of the compilation's users
-beyond what the individual works permit. Inclusion of a covered work
-in an aggregate does not cause this License to apply to the other
-parts of the aggregate.
-
- 6. Conveying Non-Source Forms.
-
- You may convey a covered work in object code form under the terms
-of sections 4 and 5, provided that you also convey the
-machine-readable Corresponding Source under the terms of this License,
-in one of these ways:
-
- a) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by the
- Corresponding Source fixed on a durable physical medium
- customarily used for software interchange.
-
- b) Convey the object code in, or embodied in, a physical product
- (including a physical distribution medium), accompanied by a
- written offer, valid for at least three years and valid for as
- long as you offer spare parts or customer support for that product
- model, to give anyone who possesses the object code either (1) a
- copy of the Corresponding Source for all the software in the
- product that is covered by this License, on a durable physical
- medium customarily used for software interchange, for a price no
- more than your reasonable cost of physically performing this
- conveying of source, or (2) access to copy the
- Corresponding Source from a network server at no charge.
-
- c) Convey individual copies of the object code with a copy of the
- written offer to provide the Corresponding Source. This
- alternative is allowed only occasionally and noncommercially, and
- only if you received the object code with such an offer, in accord
- with subsection 6b.
-
- d) Convey the object code by offering access from a designated
- place (gratis or for a charge), and offer equivalent access to the
- Corresponding Source in the same way through the same place at no
- further charge. You need not require recipients to copy the
- Corresponding Source along with the object code. If the place to
- copy the object code is a network server, the Corresponding Source
- may be on a different server (operated by you or a third party)
- that supports equivalent copying facilities, provided you maintain
- clear directions next to the object code saying where to find the
- Corresponding Source. Regardless of what server hosts the
- Corresponding Source, you remain obligated to ensure that it is
- available for as long as needed to satisfy these requirements.
-
- e) Convey the object code using peer-to-peer transmission, provided
- you inform other peers where the object code and Corresponding
- Source of the work are being offered to the general public at no
- charge under subsection 6d.
-
- A separable portion of the object code, whose source code is excluded
-from the Corresponding Source as a System Library, need not be
-included in conveying the object code work.
-
- A "User Product" is either (1) a "consumer product", which means any
-tangible personal property which is normally used for personal, family,
-or household purposes, or (2) anything designed or sold for incorporation
-into a dwelling. In determining whether a product is a consumer product,
-doubtful cases shall be resolved in favor of coverage. For a particular
-product received by a particular user, "normally used" refers to a
-typical or common use of that class of product, regardless of the status
-of the particular user or of the way in which the particular user
-actually uses, or expects or is expected to use, the product. A product
-is a consumer product regardless of whether the product has substantial
-commercial, industrial or non-consumer uses, unless such uses represent
-the only significant mode of use of the product.
-
- "Installation Information" for a User Product means any methods,
-procedures, authorization keys, or other information required to install
-and execute modified versions of a covered work in that User Product from
-a modified version of its Corresponding Source. The information must
-suffice to ensure that the continued functioning of the modified object
-code is in no case prevented or interfered with solely because
-modification has been made.
-
- If you convey an object code work under this section in, or with, or
-specifically for use in, a User Product, and the conveying occurs as
-part of a transaction in which the right of possession and use of the
-User Product is transferred to the recipient in perpetuity or for a
-fixed term (regardless of how the transaction is characterized), the
-Corresponding Source conveyed under this section must be accompanied
-by the Installation Information. But this requirement does not apply
-if neither you nor any third party retains the ability to install
-modified object code on the User Product (for example, the work has
-been installed in ROM).
-
- The requirement to provide Installation Information does not include a
-requirement to continue to provide support service, warranty, or updates
-for a work that has been modified or installed by the recipient, or for
-the User Product in which it has been modified or installed. Access to a
-network may be denied when the modification itself materially and
-adversely affects the operation of the network or violates the rules and
-protocols for communication across the network.
-
- Corresponding Source conveyed, and Installation Information provided,
-in accord with this section must be in a format that is publicly
-documented (and with an implementation available to the public in
-source code form), and must require no special password or key for
-unpacking, reading or copying.
-
- 7. Additional Terms.
-
- "Additional permissions" are terms that supplement the terms of this
-License by making exceptions from one or more of its conditions.
-Additional permissions that are applicable to the entire Program shall
-be treated as though they were included in this License, to the extent
-that they are valid under applicable law. If additional permissions
-apply only to part of the Program, that part may be used separately
-under those permissions, but the entire Program remains governed by
-this License without regard to the additional permissions.
-
- When you convey a copy of a covered work, you may at your option
-remove any additional permissions from that copy, or from any part of
-it. (Additional permissions may be written to require their own
-removal in certain cases when you modify the work.) You may place
-additional permissions on material, added by you to a covered work,
-for which you have or can give appropriate copyright permission.
-
- Notwithstanding any other provision of this License, for material you
-add to a covered work, you may (if authorized by the copyright holders of
-that material) supplement the terms of this License with terms:
-
- a) Disclaiming warranty or limiting liability differently from the
- terms of sections 15 and 16 of this License; or
-
- b) Requiring preservation of specified reasonable legal notices or
- author attributions in that material or in the Appropriate Legal
- Notices displayed by works containing it; or
-
- c) Prohibiting misrepresentation of the origin of that material, or
- requiring that modified versions of such material be marked in
- reasonable ways as different from the original version; or
-
- d) Limiting the use for publicity purposes of names of licensors or
- authors of the material; or
-
- e) Declining to grant rights under trademark law for use of some
- trade names, trademarks, or service marks; or
-
- f) Requiring indemnification of licensors and authors of that
- material by anyone who conveys the material (or modified versions of
- it) with contractual assumptions of liability to the recipient, for
- any liability that these contractual assumptions directly impose on
- those licensors and authors.
-
- All other non-permissive additional terms are considered "further
-restrictions" within the meaning of section 10. If the Program as you
-received it, or any part of it, contains a notice stating that it is
-governed by this License along with a term that is a further
-restriction, you may remove that term. If a license document contains
-a further restriction but permits relicensing or conveying under this
-License, you may add to a covered work material governed by the terms
-of that license document, provided that the further restriction does
-not survive such relicensing or conveying.
-
- If you add terms to a covered work in accord with this section, you
-must place, in the relevant source files, a statement of the
-additional terms that apply to those files, or a notice indicating
-where to find the applicable terms.
-
- Additional terms, permissive or non-permissive, may be stated in the
-form of a separately written license, or stated as exceptions;
-the above requirements apply either way.
-
- 8. Termination.
-
- You may not propagate or modify a covered work except as expressly
-provided under this License. Any attempt otherwise to propagate or
-modify it is void, and will automatically terminate your rights under
-this License (including any patent licenses granted under the third
-paragraph of section 11).
-
- However, if you cease all violation of this License, then your
-license from a particular copyright holder is reinstated (a)
-provisionally, unless and until the copyright holder explicitly and
-finally terminates your license, and (b) permanently, if the copyright
-holder fails to notify you of the violation by some reasonable means
-prior to 60 days after the cessation.
-
- Moreover, your license from a particular copyright holder is
-reinstated permanently if the copyright holder notifies you of the
-violation by some reasonable means, this is the first time you have
-received notice of violation of this License (for any work) from that
-copyright holder, and you cure the violation prior to 30 days after
-your receipt of the notice.
-
- Termination of your rights under this section does not terminate the
-licenses of parties who have received copies or rights from you under
-this License. If your rights have been terminated and not permanently
-reinstated, you do not qualify to receive new licenses for the same
-material under section 10.
-
- 9. Acceptance Not Required for Having Copies.
-
- You are not required to accept this License in order to receive or
-run a copy of the Program. Ancillary propagation of a covered work
-occurring solely as a consequence of using peer-to-peer transmission
-to receive a copy likewise does not require acceptance. However,
-nothing other than this License grants you permission to propagate or
-modify any covered work. These actions infringe copyright if you do
-not accept this License. Therefore, by modifying or propagating a
-covered work, you indicate your acceptance of this License to do so.
-
- 10. Automatic Licensing of Downstream Recipients.
-
- Each time you convey a covered work, the recipient automatically
-receives a license from the original licensors, to run, modify and
-propagate that work, subject to this License. You are not responsible
-for enforcing compliance by third parties with this License.
-
- An "entity transaction" is a transaction transferring control of an
-organization, or substantially all assets of one, or subdividing an
-organization, or merging organizations. If propagation of a covered
-work results from an entity transaction, each party to that
-transaction who receives a copy of the work also receives whatever
-licenses to the work the party's predecessor in interest had or could
-give under the previous paragraph, plus a right to possession of the
-Corresponding Source of the work from the predecessor in interest, if
-the predecessor has it or can get it with reasonable efforts.
-
- You may not impose any further restrictions on the exercise of the
-rights granted or affirmed under this License. For example, you may
-not impose a license fee, royalty, or other charge for exercise of
-rights granted under this License, and you may not initiate litigation
-(including a cross-claim or counterclaim in a lawsuit) alleging that
-any patent claim is infringed by making, using, selling, offering for
-sale, or importing the Program or any portion of it.
-
- 11. Patents.
-
- A "contributor" is a copyright holder who authorizes use under this
-License of the Program or a work on which the Program is based. The
-work thus licensed is called the contributor's "contributor version".
-
- A contributor's "essential patent claims" are all patent claims
-owned or controlled by the contributor, whether already acquired or
-hereafter acquired, that would be infringed by some manner, permitted
-by this License, of making, using, or selling its contributor version,
-but do not include claims that would be infringed only as a
-consequence of further modification of the contributor version. For
-purposes of this definition, "control" includes the right to grant
-patent sublicenses in a manner consistent with the requirements of
-this License.
-
- Each contributor grants you a non-exclusive, worldwide, royalty-free
-patent license under the contributor's essential patent claims, to
-make, use, sell, offer for sale, import and otherwise run, modify and
-propagate the contents of its contributor version.
-
- In the following three paragraphs, a "patent license" is any express
-agreement or commitment, however denominated, not to enforce a patent
-(such as an express permission to practice a patent or covenant not to
-sue for patent infringement). To "grant" such a patent license to a
-party means to make such an agreement or commitment not to enforce a
-patent against the party.
-
- If you convey a covered work, knowingly relying on a patent license,
-and the Corresponding Source of the work is not available for anyone
-to copy, free of charge and under the terms of this License, through a
-publicly available network server or other readily accessible means,
-then you must either (1) cause the Corresponding Source to be so
-available, or (2) arrange to deprive yourself of the benefit of the
-patent license for this particular work, or (3) arrange, in a manner
-consistent with the requirements of this License, to extend the patent
-license to downstream recipients. "Knowingly relying" means you have
-actual knowledge that, but for the patent license, your conveying the
-covered work in a country, or your recipient's use of the covered work
-in a country, would infringe one or more identifiable patents in that
-country that you have reason to believe are valid.
-
- If, pursuant to or in connection with a single transaction or
-arrangement, you convey, or propagate by procuring conveyance of, a
-covered work, and grant a patent license to some of the parties
-receiving the covered work authorizing them to use, propagate, modify
-or convey a specific copy of the covered work, then the patent license
-you grant is automatically extended to all recipients of the covered
-work and works based on it.
-
- A patent license is "discriminatory" if it does not include within
-the scope of its coverage, prohibits the exercise of, or is
-conditioned on the non-exercise of one or more of the rights that are
-specifically granted under this License. You may not convey a covered
-work if you are a party to an arrangement with a third party that is
-in the business of distributing software, under which you make payment
-to the third party based on the extent of your activity of conveying
-the work, and under which the third party grants, to any of the
-parties who would receive the covered work from you, a discriminatory
-patent license (a) in connection with copies of the covered work
-conveyed by you (or copies made from those copies), or (b) primarily
-for and in connection with specific products or compilations that
-contain the covered work, unless you entered into that arrangement,
-or that patent license was granted, prior to 28 March 2007.
-
- Nothing in this License shall be construed as excluding or limiting
-any implied license or other defenses to infringement that may
-otherwise be available to you under applicable patent law.
-
- 12. No Surrender of Others' Freedom.
-
- If conditions are imposed on you (whether by court order, agreement or
-otherwise) that contradict the conditions of this License, they do not
-excuse you from the conditions of this License. If you cannot convey a
-covered work so as to satisfy simultaneously your obligations under this
-License and any other pertinent obligations, then as a consequence you may
-not convey it at all. For example, if you agree to terms that obligate you
-to collect a royalty for further conveying from those to whom you convey
-the Program, the only way you could satisfy both those terms and this
-License would be to refrain entirely from conveying the Program.
-
- 13. Use with the GNU Affero General Public License.
-
- Notwithstanding any other provision of this License, you have
-permission to link or combine any covered work with a work licensed
-under version 3 of the GNU Affero General Public License into a single
-combined work, and to convey the resulting work. The terms of this
-License will continue to apply to the part which is the covered work,
-but the special requirements of the GNU Affero General Public License,
-section 13, concerning interaction through a network will apply to the
-combination as such.
-
- 14. Revised Versions of this License.
-
- The Free Software Foundation may publish revised and/or new versions of
-the GNU General Public License from time to time. Such new versions will
-be similar in spirit to the present version, but may differ in detail to
-address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Program specifies that a certain numbered version of the GNU General
-Public License "or any later version" applies to it, you have the
-option of following the terms and conditions either of that numbered
-version or of any later version published by the Free Software
-Foundation. If the Program does not specify a version number of the
-GNU General Public License, you may choose any version ever published
-by the Free Software Foundation.
-
- If the Program specifies that a proxy can decide which future
-versions of the GNU General Public License can be used, that proxy's
-public statement of acceptance of a version permanently authorizes you
-to choose that version for the Program.
-
- Later license versions may give you additional or different
-permissions. However, no additional obligations are imposed on any
-author or copyright holder as a result of your choosing to follow a
-later version.
-
- 15. Disclaimer of Warranty.
-
- THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
-APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
-HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
-OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
-THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
-PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
-IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
-ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
-
- 16. Limitation of Liability.
-
- IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
-WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
-THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
-GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
-USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
-DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
-PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
-EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
-SUCH DAMAGES.
-
- 17. Interpretation of Sections 15 and 16.
-
- If the disclaimer of warranty and limitation of liability provided
-above cannot be given local legal effect according to their terms,
-reviewing courts shall apply local law that most closely approximates
-an absolute waiver of all civil liability in connection with the
-Program, unless a warranty or assumption of liability accompanies a
-copy of the Program in return for a fee.
-
- END OF TERMS AND CONDITIONS
-
- How to Apply These Terms to Your New Programs
-
- If you develop a new program, and you want it to be of the greatest
-possible use to the public, the best way to achieve this is to make it
-free software which everyone can redistribute and change under these terms.
-
- To do so, attach the following notices to the program. It is safest
-to attach them to the start of each source file to most effectively
-state the exclusion of warranty; and each file should have at least
-the "copyright" line and a pointer to where the full notice is found.
-
-
- Copyright (C)
-
- This program is free software: you can redistribute it and/or modify
- it under the terms of the GNU General Public License as published by
- the Free Software Foundation, either version 3 of the License, or
- (at your option) any later version.
-
- This program is distributed in the hope that it will be useful,
- but WITHOUT ANY WARRANTY; without even the implied warranty of
- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- GNU General Public License for more details.
-
- You should have received a copy of the GNU General Public License
- along with this program. If not, see .
-
-Also add information on how to contact you by electronic and paper mail.
-
- If the program does terminal interaction, make it output a short
-notice like this when it starts in an interactive mode:
-
- Copyright (C)
- This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
- This is free software, and you are welcome to redistribute it
- under certain conditions; type `show c' for details.
-
-The hypothetical commands `show w' and `show c' should show the appropriate
-parts of the General Public License. Of course, your program's commands
-might be different; for a GUI interface, you would use an "about box".
-
- You should also get your employer (if you work as a programmer) or school,
-if any, to sign a "copyright disclaimer" for the program, if necessary.
-For more information on this, and how to apply and follow the GNU GPL, see
-.
-
- The GNU General Public License does not permit incorporating your program
-into proprietary programs. If your program is a subroutine library, you
-may consider it more useful to permit linking proprietary applications with
-the library. If this is what you want to do, use the GNU Lesser General
-Public License instead of this License. But first, please read
-.
+MIT License
+
+Copyright (c) 2024 SiYuan
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
\ No newline at end of file
diff --git a/app/DataBase/__init__.py b/app/DataBase/__init__.py
deleted file mode 100644
index 5c44b10..0000000
--- a/app/DataBase/__init__.py
+++ /dev/null
@@ -1,40 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-@File : __init__.py.py
-@Author : Shuaikang Zhou
-@Time : 2023/1/5 0:10
-@IDE : Pycharm
-@Version : Python3.10
-@comment : ···
-"""
-from .hard_link import HardLink
-from .micro_msg import MicroMsg
-from .media_msg import MediaMsg
-from .misc import Misc
-from .msg import Msg
-from .msg import MsgType
-
-misc_db = Misc()
-msg_db = Msg()
-micro_msg_db = MicroMsg()
-hard_link_db = HardLink()
-media_msg_db = MediaMsg()
-
-
-def close_db():
- misc_db.close()
- msg_db.close()
- micro_msg_db.close()
- hard_link_db.close()
- media_msg_db.close()
-
-
-def init_db():
- misc_db.init_database()
- msg_db.init_database()
- micro_msg_db.init_database()
- hard_link_db.init_database()
- media_msg_db.init_database()
-
-
-__all__ = ['misc_db', 'micro_msg_db', 'msg_db', 'hard_link_db', 'MsgType', "media_msg_db", "close_db"]
diff --git a/app/DataBase/hard_link.py b/app/DataBase/hard_link.py
deleted file mode 100644
index 95cf643..0000000
--- a/app/DataBase/hard_link.py
+++ /dev/null
@@ -1,297 +0,0 @@
-import binascii
-import os.path
-import sqlite3
-import threading
-import traceback
-import xml.etree.ElementTree as ET
-
-from app.log import log, logger
-from app.util.protocbuf.msg_pb2 import MessageBytesExtra
-
-image_db_lock = threading.Lock()
-video_db_lock = threading.Lock()
-image_db_path = "./app/Database/Msg/HardLinkImage.db"
-video_db_path = "./app/Database/Msg/HardLinkVideo.db"
-root_path = "FileStorage/MsgAttach/"
-video_root_path = "FileStorage/Video/"
-
-
-@log
-def get_md5_from_xml(content, type_="img"):
- try:
- # 解析XML
- root = ET.fromstring(content)
- if type_ == "img":
- # 提取md5的值
- md5_value = root.find(".//img").get("md5")
- elif type_ == "video":
- md5_value = root.find(".//videomsg").get("md5")
- # print(md5_value)
- return md5_value
- except ET.ParseError:
- return None
-
-
-def decodeExtraBuf(extra_buf_content: bytes):
- if not extra_buf_content:
- return {
- "region": ('', '', ''),
- "signature": '',
- "telephone": '',
- "gender": 0,
- }
- trunkName = {
- b"\x46\xCF\x10\xC4": "个性签名",
- b"\xA4\xD9\x02\x4A": "国家",
- b"\xE2\xEA\xA8\xD1": "省份",
- b"\x1D\x02\x5B\xBF": "市",
- # b"\x81\xAE\x19\xB4": "朋友圈背景url",
- # b"\xF9\x17\xBC\xC0": "公司名称",
- # b"\x4E\xB9\x6D\x85": "企业微信属性",
- # b"\x0E\x71\x9F\x13": "备注图片",
- b"\x75\x93\x78\xAD": "手机号",
- b"\x74\x75\x2C\x06": "性别",
- }
- res = {"手机号": ""}
- off = 0
- try:
- for key in trunkName:
- trunk_head = trunkName[key]
- try:
- off = extra_buf_content.index(key) + 4
- except:
- pass
- char = extra_buf_content[off: off + 1]
- off += 1
- if char == b"\x04": # 四个字节的int,小端序
- intContent = extra_buf_content[off: off + 4]
- off += 4
- intContent = int.from_bytes(intContent, "little")
- res[trunk_head] = intContent
- elif char == b"\x18": # utf-16字符串
- lengthContent = extra_buf_content[off: off + 4]
- off += 4
- lengthContent = int.from_bytes(lengthContent, "little")
- strContent = extra_buf_content[off: off + lengthContent]
- off += lengthContent
- res[trunk_head] = strContent.decode("utf-16").rstrip("\x00")
- return {
- "region": (res["国家"], res["省份"], res["市"]),
- "signature": res["个性签名"],
- "telephone": res["手机号"],
- "gender": res["性别"],
- }
- except:
- logger.error(f'联系人解析错误:\n{traceback.format_exc()}')
- return {
- "region": ('', '', ''),
- "signature": '',
- "telephone": '',
- "gender": 0,
- }
-
-
-def singleton(cls):
- _instance = {}
-
- def inner():
- if cls not in _instance:
- _instance[cls] = cls()
- return _instance[cls]
-
- return inner
-
-
-@singleton
-class HardLink:
- def __init__(self):
- self.imageDB = None
- self.videoDB = None
- self.image_cursor = None
- self.video_cursor = None
- self.open_flag = False
- self.init_database()
-
- def init_database(self):
- if not self.open_flag:
- if os.path.exists(image_db_path):
- self.imageDB = sqlite3.connect(image_db_path, check_same_thread=False)
- # '''创建游标'''
- self.image_cursor = self.imageDB.cursor()
- self.open_flag = True
- if image_db_lock.locked():
- image_db_lock.release()
- if os.path.exists(video_db_path):
- self.videoDB = sqlite3.connect(video_db_path, check_same_thread=False)
- # '''创建游标'''
- self.video_cursor = self.videoDB.cursor()
- self.open_flag = True
- if video_db_lock.locked():
- video_db_lock.release()
-
- def get_image_by_md5(self, md5: bytes):
- if not md5:
- return None
- if not self.open_flag:
- return None
- sql = """
- select Md5Hash,MD5,FileName,HardLinkImageID.Dir as DirName1,HardLinkImageID2.Dir as DirName2
- from HardLinkImageAttribute
- join HardLinkImageID on HardLinkImageAttribute.DirID1 = HardLinkImageID.DirID
- join HardLinkImageID as HardLinkImageID2 on HardLinkImageAttribute.DirID2 = HardLinkImageID2.DirID
- where MD5 = ?;
- """
- try:
- image_db_lock.acquire(True)
- try:
- self.image_cursor.execute(sql, [md5])
- except AttributeError:
- self.init_database()
- self.image_cursor.execute(sql, [md5])
- result = self.image_cursor.fetchone()
- return result
- finally:
- image_db_lock.release()
-
- def get_video_by_md5(self, md5: bytes):
- if not md5:
- return None
- if not self.open_flag:
- return None
- sql = """
- select Md5Hash,MD5,FileName,HardLinkVideoID2.Dir as DirName2
- from HardLinkVideoAttribute
- join HardLinkVideoID as HardLinkVideoID2 on HardLinkVideoAttribute.DirID2 = HardLinkVideoID2.DirID
- where MD5 = ?;
- """
- try:
- video_db_lock.acquire(True)
- try:
- self.video_cursor.execute(sql, [md5])
- except sqlite3.OperationalError:
- return None
- except AttributeError:
- self.init_database()
- self.video_cursor.execute(sql, [md5])
- result = self.video_cursor.fetchone()
- return result
- finally:
- video_db_lock.release()
-
- def get_image_original(self, content, bytesExtra) -> str:
- msg_bytes = MessageBytesExtra()
- msg_bytes.ParseFromString(bytesExtra)
- result = ''
- for tmp in msg_bytes.message2:
- if tmp.field1 != 4:
- continue
- pathh = tmp.field2 # wxid\FileStorage\...
- pathh = "\\".join(pathh.split("\\")[1:])
- return pathh
- md5 = get_md5_from_xml(content)
- if not md5:
- pass
- else:
- result = self.get_image_by_md5(binascii.unhexlify(md5))
- if result:
- dir1 = result[3]
- dir2 = result[4]
- data_image = result[2]
- dir0 = "Image"
- dat_image = os.path.join(root_path, dir1, dir0, dir2, data_image)
- result = dat_image
- return result
-
- def get_image_thumb(self, content, bytesExtra) -> str:
- msg_bytes = MessageBytesExtra()
- msg_bytes.ParseFromString(bytesExtra)
- result = ''
- for tmp in msg_bytes.message2:
- if tmp.field1 != 3:
- continue
- pathh = tmp.field2 # wxid\FileStorage\...
- pathh = "\\".join(pathh.split("\\")[1:])
- return pathh
- md5 = get_md5_from_xml(content)
- if not md5:
- pass
- else:
- result = self.get_image_by_md5(binascii.unhexlify(md5))
- if result:
- dir1 = result[3]
- dir2 = result[4]
- data_image = result[2]
- dir0 = "Thumb"
- dat_image = os.path.join(root_path, dir1, dir0, dir2, data_image)
- result = dat_image
- return result
-
- def get_image(self, content, bytesExtra, up_dir="", thumb=False) -> str:
- msg_bytes = MessageBytesExtra()
- msg_bytes.ParseFromString(bytesExtra)
- if thumb:
- result = self.get_image_thumb(content, bytesExtra)
- else:
- result = self.get_image_original(content, bytesExtra)
- if not (result and os.path.exists(os.path.join(up_dir, result))):
- result = self.get_image_thumb(content, bytesExtra)
- return result
-
- def get_video(self, content, bytesExtra, thumb=False):
- msg_bytes = MessageBytesExtra()
- msg_bytes.ParseFromString(bytesExtra)
- for tmp in msg_bytes.message2:
- if tmp.field1 != (3 if thumb else 4):
- continue
- pathh = tmp.field2 # wxid\FileStorage\...
- pathh = "\\".join(pathh.split("\\")[1:])
- return pathh
- md5 = get_md5_from_xml(content, type_="video")
- if not md5:
- return ''
- result = self.get_video_by_md5(binascii.unhexlify(md5))
- if result:
- dir2 = result[3]
- data_image = result[2].split(".")[0] + ".jpg" if thumb else result[2]
- # dir0 = 'Thumb' if thumb else 'Image'
- dat_image = os.path.join(video_root_path, dir2, data_image)
- return dat_image
- else:
- return ''
-
- def close(self):
- if self.open_flag:
- try:
- image_db_lock.acquire(True)
- video_db_lock.acquire(True)
- self.open_flag = False
- self.imageDB.close()
- self.videoDB.close()
- finally:
- image_db_lock.release()
- video_db_lock.release()
-
- def __del__(self):
- self.close()
-
-
-# 6b02292eecea118f06be3a5b20075afc_t
-
-if __name__ == "__main__":
- msg_root_path = "./Msg/"
- image_db_path = "./Msg/HardLinkImage.db"
- video_db_path = "./Msg/HardLinkVideo.db"
- hard_link_db = HardLink()
- hard_link_db.init_database()
- # content = '''\n\t
\n\t\n\t\n\n'''
- # print(hard_link_db.get_image(content))
- # print(hard_link_db.get_image(content, thumb=False))
- # result = get_md5_from_xml(content)
- # print(result)
- content = """
-
-
-
-"""
- print(hard_link_db.get_video(content))
- print(hard_link_db.get_video(content, thumb=True))
diff --git a/app/DataBase/media_msg.py b/app/DataBase/media_msg.py
deleted file mode 100644
index 4baf912..0000000
--- a/app/DataBase/media_msg.py
+++ /dev/null
@@ -1,145 +0,0 @@
-import os.path
-import subprocess
-import sys
-import traceback
-from os import system
-import sqlite3
-import threading
-import xml.etree.ElementTree as ET
-from pilk import decode
-
-from app.log import logger
-
-lock = threading.Lock()
-db_path = "./app/Database/Msg/MediaMSG.db"
-
-
-def get_ffmpeg_path():
- # 获取打包后的资源目录
- resource_dir = getattr(sys, '_MEIPASS', os.path.abspath(os.path.dirname(__file__)))
-
- # 构建 FFmpeg 可执行文件的路径
- ffmpeg_path = os.path.join(resource_dir, 'app', 'resources','data', 'ffmpeg.exe')
-
- return ffmpeg_path
-
-
-def singleton(cls):
- _instance = {}
-
- def inner():
- if cls not in _instance:
- _instance[cls] = cls()
- return _instance[cls]
-
- return inner
-
-
-@singleton
-class MediaMsg:
- def __init__(self):
- self.DB = None
- self.cursor: sqlite3.Cursor = None
- self.open_flag = False
- self.init_database()
-
- def init_database(self):
- if not self.open_flag:
- if os.path.exists(db_path):
- self.DB = sqlite3.connect(db_path, check_same_thread=False)
- # '''创建游标'''
- self.cursor = self.DB.cursor()
- self.open_flag = True
- if lock.locked():
- lock.release()
-
- def get_media_buffer(self, reserved0):
- sql = '''
- select Buf
- from Media
- where Reserved0 = ?
- '''
- try:
- lock.acquire(True)
- self.cursor.execute(sql, [reserved0])
- result = self.cursor.fetchone()
-
- finally:
- lock.release()
- return result[0] if result else None
-
- def get_audio(self, reserved0, output_path):
- buf = self.get_media_buffer(reserved0)
- if not buf:
- return ''
- silk_path = f"{output_path}/{reserved0}.silk"
- pcm_path = f"{output_path}/{reserved0}.pcm"
- mp3_path = f"{output_path}/{reserved0}.mp3"
- if os.path.exists(mp3_path):
- return mp3_path
- with open(silk_path, "wb") as f:
- f.write(buf)
- # open(silk_path, "wb").write()
- try:
- decode(silk_path, pcm_path, 44100)
- # 调用系统上的 ffmpeg 可执行文件
- # 获取 FFmpeg 可执行文件的路径
- ffmpeg_path = get_ffmpeg_path()
- # # 调用 FFmpeg
- if os.path.exists(ffmpeg_path):
- cmd = f'''"{ffmpeg_path}" -loglevel quiet -y -f s16le -i "{pcm_path}" -ar 44100 -ac 1 "{mp3_path}"'''
- # system(cmd)
- # 使用subprocess.run()执行命令
- subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- else:
- # 源码运行的时候下面的有效
- # 这里不知道怎么捕捉异常
- cmd = f'''"{os.path.join(os.getcwd(), 'app', 'resources', 'data','ffmpeg.exe')}" -loglevel quiet -y -f s16le -i "{pcm_path}" -ar 44100 -ac 1 "{mp3_path}"'''
- # system(cmd)
- # 使用subprocess.run()执行命令
- subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- os.remove(silk_path)
- os.remove(pcm_path)
- except Exception as e:
- print(f"Error: {e}")
- logger.error(f'语音发送错误\n{traceback.format_exc()}')
- cmd = f'''"{os.path.join(os.getcwd(), 'app', 'resources', 'data', 'ffmpeg.exe')}" -loglevel quiet -y -f s16le -i "{pcm_path}" -ar 44100 -ac 1 "{mp3_path}"'''
- # system(cmd)
- # 使用subprocess.run()执行命令
- subprocess.run(cmd, shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
- finally:
- print(mp3_path)
- return mp3_path
-
- def get_audio_path(self, reserved0, output_path):
- mp3_path = f"{output_path}\\{reserved0}.mp3"
- mp3_path = mp3_path.replace("/", "\\")
- return mp3_path
-
- def get_audio_text(self, content):
- try:
- root = ET.fromstring(content)
- transtext = root.find(".//voicetrans").get("transtext")
- return transtext
- except:
- return ""
-
- def close(self):
- if self.open_flag:
- try:
- lock.acquire(True)
- self.open_flag = False
- self.DB.close()
- finally:
- lock.release()
-
- def __del__(self):
- self.close()
-
-
-if __name__ == '__main__':
- db_path = './Msg/MediaMSG.db'
- media_msg_db = MediaMsg()
- reserved = '2865682741418252473'
- path = media_msg_db.get_audio(reserved, r"D:\gou\message\WeChatMsg")
- print(path)
diff --git a/app/DataBase/merge.py b/app/DataBase/merge.py
deleted file mode 100644
index 945eba0..0000000
--- a/app/DataBase/merge.py
+++ /dev/null
@@ -1,103 +0,0 @@
-import os
-import sqlite3
-import traceback
-
-from app.log import logger
-
-
-def merge_MediaMSG_databases(source_paths, target_path):
- # 创建目标数据库连接
- target_conn = sqlite3.connect(target_path)
- target_cursor = target_conn.cursor()
- try:
- # 开始事务
- target_conn.execute("BEGIN;")
- for i, source_path in enumerate(source_paths):
- if not os.path.exists(source_path):
- continue
- db = sqlite3.connect(source_path)
- db.text_factory = str
- cursor = db.cursor()
- # 附加源数据库
- try:
- sql = '''SELECT Key,Reserved0,Buf,Reserved1,Reserved2 FROM Media;'''
- cursor.execute(sql)
- result = cursor.fetchall()
- target_cursor.executemany(
- "INSERT INTO Media (Key,Reserved0,Buf,Reserved1,Reserved2)"
- "VALUES(?,?,?,?,?)",
- result)
- except sqlite3.IntegrityError:
- print("有重复key", "跳过")
- except sqlite3.OperationalError:
- print("no such table: Media", "跳过")
- cursor.close()
- db.close()
- # 提交事务
- target_conn.execute("COMMIT;")
-
- except Exception as e:
- # 发生异常时回滚事务
- target_conn.execute("ROLLBACK;")
- raise e
-
- finally:
- # 关闭目标数据库连接
- target_conn.close()
-
-
-def merge_databases(source_paths, target_path):
- # 创建目标数据库连接
- target_conn = sqlite3.connect(target_path)
- target_cursor = target_conn.cursor()
- try:
- # 开始事务
- target_conn.execute("BEGIN;")
- for i, source_path in enumerate(source_paths):
- if not os.path.exists(source_path):
- continue
- db = sqlite3.connect(source_path)
- db.text_factory = str
- cursor = db.cursor()
- try:
- sql = '''
- SELECT TalkerId,MsgsvrID,Type,SubType,IsSender,CreateTime,Sequence,StrTalker,StrContent,DisplayContent,BytesExtra,CompressContent
- FROM MSG;
- '''
- cursor.execute(sql)
- result = cursor.fetchall()
- # 附加源数据库
- target_cursor.executemany(
- "INSERT INTO MSG "
- "(TalkerId,MsgsvrID,Type,SubType,IsSender,CreateTime,Sequence,StrTalker,StrContent,DisplayContent,"
- "BytesExtra,CompressContent)"
- "VALUES(?,?,?,?,?,?,?,?,?,?,?,?)",
- result)
- except:
- logger.error(f'{source_path}数据库合并错误:\n{traceback.format_exc()}')
- cursor.close()
- db.close()
- # 提交事务
- target_conn.execute("COMMIT;")
-
- except Exception as e:
- # 发生异常时回滚事务
- target_conn.execute("ROLLBACK;")
- raise e
-
- finally:
- # 关闭目标数据库连接
- target_conn.close()
-
-
-if __name__ == "__main__":
- # 源数据库文件列表
- source_databases = ["Msg/MSG1.db", "Msg/MSG2.db", "Msg/MSG3.db"]
-
- # 目标数据库文件
- target_database = "Msg/MSG.db"
- import shutil
-
- shutil.copy('Msg/MSG0.db', target_database) # 使用一个数据库文件作为模板
- # 合并数据库
- merge_databases(source_databases, target_database)
diff --git a/app/DataBase/micro_msg.py b/app/DataBase/micro_msg.py
deleted file mode 100644
index 44ae061..0000000
--- a/app/DataBase/micro_msg.py
+++ /dev/null
@@ -1,152 +0,0 @@
-import os.path
-import sqlite3
-import threading
-
-lock = threading.Lock()
-db_path = "./app/Database/Msg/MicroMsg.db"
-
-
-def singleton(cls):
- _instance = {}
-
- def inner():
- if cls not in _instance:
- _instance[cls] = cls()
- return _instance[cls]
-
- return inner
-
-
-def is_database_exist():
- return os.path.exists(db_path)
-
-
-class MicroMsg:
- def __init__(self):
- self.DB = None
- self.cursor = None
- self.open_flag = False
- self.init_database()
-
- def init_database(self):
- if not self.open_flag:
- if os.path.exists(db_path):
- self.DB = sqlite3.connect(db_path, check_same_thread=False)
- # '''创建游标'''
- self.cursor = self.DB.cursor()
- self.open_flag = True
- if lock.locked():
- lock.release()
-
- def get_contact(self):
- if not self.open_flag:
- return []
- try:
- lock.acquire(True)
- sql = '''SELECT UserName, Alias, Type, Remark, NickName, PYInitial, RemarkPYInitial, ContactHeadImgUrl.smallHeadImgUrl, ContactHeadImgUrl.bigHeadImgUrl,ExTraBuf,COALESCE(ContactLabel.LabelName, 'None') AS labelName
- FROM Contact
- INNER JOIN ContactHeadImgUrl ON Contact.UserName = ContactHeadImgUrl.usrName
- LEFT JOIN ContactLabel ON Contact.LabelIDList = ContactLabel.LabelId
- WHERE (Type!=4 AND VerifyFlag=0)
- AND NickName != ''
- ORDER BY
- CASE
- WHEN RemarkPYInitial = '' THEN PYInitial
- ELSE RemarkPYInitial
- END ASC
- '''
- self.cursor.execute(sql)
- result = self.cursor.fetchall()
- except sqlite3.OperationalError:
- # lock.acquire(True)
- sql = '''
- SELECT UserName, Alias, Type, Remark, NickName, PYInitial, RemarkPYInitial, ContactHeadImgUrl.smallHeadImgUrl, ContactHeadImgUrl.bigHeadImgUrl,ExTraBuf,"None"
- FROM Contact
- INNER JOIN ContactHeadImgUrl ON Contact.UserName = ContactHeadImgUrl.usrName
- WHERE (Type!=4 AND VerifyFlag=0)
- AND NickName != ''
- ORDER BY
- CASE
- WHEN RemarkPYInitial = '' THEN PYInitial
- ELSE RemarkPYInitial
- END ASC
- '''
- self.cursor.execute(sql)
- result = self.cursor.fetchall()
- finally:
- lock.release()
- from app.DataBase import msg_db
- return msg_db.get_contact(result)
-
- def get_contact_by_username(self, username):
- if not self.open_flag:
- return None
- try:
- lock.acquire(True)
- sql = '''
- SELECT UserName, Alias, Type, Remark, NickName, PYInitial, RemarkPYInitial, ContactHeadImgUrl.smallHeadImgUrl, ContactHeadImgUrl.bigHeadImgUrl,ExTraBuf,ContactLabel.LabelName
- FROM Contact
- INNER JOIN ContactHeadImgUrl ON Contact.UserName = ContactHeadImgUrl.usrName
- LEFT JOIN ContactLabel ON Contact.LabelIDList = ContactLabel.LabelId
- WHERE UserName = ?
- '''
- self.cursor.execute(sql, [username])
- result = self.cursor.fetchone()
- except sqlite3.OperationalError:
- # 解决ContactLabel表不存在的问题
- # lock.acquire(True)
- sql = '''
- SELECT UserName, Alias, Type, Remark, NickName, PYInitial, RemarkPYInitial, ContactHeadImgUrl.smallHeadImgUrl, ContactHeadImgUrl.bigHeadImgUrl,ExTraBuf,"None"
- FROM Contact
- INNER JOIN ContactHeadImgUrl ON Contact.UserName = ContactHeadImgUrl.usrName
- WHERE UserName = ?
- '''
- self.cursor.execute(sql, [username])
- result = self.cursor.fetchone()
- finally:
- lock.release()
-
- return result
-
- def get_chatroom_info(self, chatroomname):
- '''
- 获取群聊信息
- '''
- if not self.open_flag:
- return None
- try:
- lock.acquire(True)
- sql = '''SELECT ChatRoomName, RoomData FROM ChatRoom WHERE ChatRoomName = ?'''
- self.cursor.execute(sql, [chatroomname])
- result = self.cursor.fetchone()
- finally:
- lock.release()
- return result
-
- def close(self):
- if self.open_flag:
- try:
- lock.acquire(True)
- self.open_flag = False
- self.DB.close()
- finally:
- lock.release()
-
- def __del__(self):
- self.close()
-
-
-if __name__ == '__main__':
- db_path = "./app/database/Msg/MicroMsg.db"
- msg = MicroMsg()
- msg.init_database()
- contacts = msg.get_contact()
- from app.DataBase.hard_link import decodeExtraBuf
-
- s = {'wxid_vtz9jk9ulzjt22','wxid_zu9l4wxdv1pa22', 'wxid_0o18ef858vnu22','wxid_8piw6sb4hvfm22','wxid_e7ypfycxpnu322','wxid_oxmg02c8kwxu22','wxid_7pp2fblq7hkq22','wxid_h1n9niofgyci22'}
- for contact in contacts:
- if contact[0] in s:
- print(contact[:7])
- buf = contact[9]
- info = decodeExtraBuf(buf)
- print(info)
diff --git a/app/DataBase/misc.py b/app/DataBase/misc.py
deleted file mode 100644
index c746247..0000000
--- a/app/DataBase/misc.py
+++ /dev/null
@@ -1,78 +0,0 @@
-import os.path
-import sqlite3
-import threading
-
-lock = threading.Lock()
-DB = None
-cursor = None
-db_path = "./app/Database/Msg/Misc.db"
-
-
-# db_path = './Msg/Misc.db'
-
-
-def singleton(cls):
- _instance = {}
-
- def inner():
- if cls not in _instance:
- _instance[cls] = cls()
- return _instance[cls]
-
- return inner
-
-
-@singleton
-class Misc:
- def __init__(self):
- self.DB = None
- self.cursor = None
- self.open_flag = False
- self.init_database()
-
- def init_database(self):
- if not self.open_flag:
- if os.path.exists(db_path):
- self.DB = sqlite3.connect(db_path, check_same_thread=False)
- # '''创建游标'''
- self.cursor = self.DB.cursor()
- self.open_flag = True
- if lock.locked():
- lock.release()
-
- def get_avatar_buffer(self, userName):
- if not self.open_flag:
- return None
- sql = '''
- select smallHeadBuf
- from ContactHeadImg1
- where usrName=?;
- '''
- if not self.open_flag:
- self.init_database()
- try:
- lock.acquire(True)
- self.cursor.execute(sql, [userName])
- result = self.cursor.fetchall()
- if result:
- return result[0][0]
- finally:
- lock.release()
- return None
-
- def close(self):
- if self.open_flag:
- try:
- lock.acquire(True)
- self.open_flag = False
- self.DB.close()
- finally:
- lock.release()
-
- def __del__(self):
- self.close()
-
-
-if __name__ == '__main__':
- Misc()
- print(Misc().get_avatar_buffer('wxid_al2oan01b6fn11'))
diff --git a/app/DataBase/msg.py b/app/DataBase/msg.py
deleted file mode 100644
index b56e671..0000000
--- a/app/DataBase/msg.py
+++ /dev/null
@@ -1,894 +0,0 @@
-import os.path
-import random
-import sqlite3
-import threading
-import traceback
-from collections import defaultdict
-from datetime import datetime, date
-from typing import Tuple
-
-from app.log import logger
-from app.util.compress_content import parser_reply
-from app.util.protocbuf.msg_pb2 import MessageBytesExtra
-
-db_path = "./app/Database/Msg/MSG.db"
-lock = threading.Lock()
-
-
-def is_database_exist():
- return os.path.exists(db_path)
-
-
-def convert_to_timestamp_(time_input) -> int:
- if isinstance(time_input, (int, float)):
- # 如果输入是时间戳,直接返回
- return int(time_input)
- elif isinstance(time_input, str):
- # 如果输入是格式化的时间字符串,将其转换为时间戳
- try:
- dt_object = datetime.strptime(time_input, '%Y-%m-%d %H:%M:%S')
- return int(dt_object.timestamp())
- except ValueError:
- # 如果转换失败,可能是其他格式的字符串,可以根据需要添加更多的处理逻辑
- print("Error: Unsupported date format")
- return -1
- elif isinstance(time_input, date):
- # 如果输入是datetime.date对象,将其转换为时间戳
- dt_object = datetime.combine(time_input, datetime.min.time())
- return int(dt_object.timestamp())
- else:
- print("Error: Unsupported input type")
- return -1
-
-
-def convert_to_timestamp(time_range) -> Tuple[int, int]:
- """
- 将时间转换成时间戳
- @param time_range:
- @return:
- """
- if not time_range:
- return 0, 0
- else:
- return convert_to_timestamp_(time_range[0]), convert_to_timestamp_(time_range[1])
-
-
-def parser_chatroom_message(messages):
- from app.DataBase import micro_msg_db, misc_db
- from app.util.protocbuf.msg_pb2 import MessageBytesExtra
- from app.person import Contact, Me, ContactDefault
- '''
- 获取一个群聊的聊天记录
- return list
- a[0]: localId,
- a[1]: talkerId, (和strtalker对应的,不是群聊信息发送人)
- a[2]: type,
- a[3]: subType,
- a[4]: is_sender,
- a[5]: timestamp,
- a[6]: status, (没啥用)
- a[7]: str_content,
- a[8]: str_time, (格式化的时间)
- a[9]: msgSvrId,
- a[10]: BytesExtra,
- a[11]: CompressContent,
- a[12]: DisplayContent,
- a[13]: msg_sender, (ContactPC 或 ContactDefault 类型,这个才是群聊里的信息发送人,不是群聊或者自己是发送者没有这个字段)
- '''
- updated_messages = [] # 用于存储修改后的消息列表
- for row in messages:
- message = list(row)
- if message[4] == 1: # 自己发送的就没必要解析了
- message.append(Me())
- updated_messages.append(tuple(message))
- continue
- if message[10] is None: # BytesExtra是空的跳过
- message.append(ContactDefault(wxid))
- updated_messages.append(tuple(message))
- continue
- msgbytes = MessageBytesExtra()
- msgbytes.ParseFromString(message[10])
- wxid = ''
- for tmp in msgbytes.message2:
- if tmp.field1 != 1:
- continue
- wxid = tmp.field2
- if wxid == "": # 系统消息里面 wxid 不存在
- message.append(ContactDefault(wxid))
- updated_messages.append(tuple(message))
- continue
- # todo 解析还是有问题,会出现这种带:的东西
- if ':' in wxid: # wxid_ewi8gfgpp0eu22:25319:1
- wxid = wxid.split(':')[0]
- contact_info_list = micro_msg_db.get_contact_by_username(wxid)
- if contact_info_list is None: # 群聊中已退群的联系人不会保存在数据库里
- message.append(ContactDefault(wxid))
- updated_messages.append(tuple(message))
- continue
- contact_info = {
- 'UserName': contact_info_list[0],
- 'Alias': contact_info_list[1],
- 'Type': contact_info_list[2],
- 'Remark': contact_info_list[3],
- 'NickName': contact_info_list[4],
- 'smallHeadImgUrl': contact_info_list[7]
- }
- contact = Contact(contact_info)
- contact.smallHeadImgBLOG = misc_db.get_avatar_buffer(contact.wxid)
- contact.set_avatar(contact.smallHeadImgBLOG)
- message.append(contact)
- updated_messages.append(tuple(message))
- return updated_messages
-
-
-def singleton(cls):
- _instance = {}
-
- def inner():
- if cls not in _instance:
- _instance[cls] = cls()
- return _instance[cls]
-
- return inner
-
-
-class MsgType:
- TEXT = 1
- IMAGE = 3
- EMOJI = 47
-
-
-class Msg:
- def __init__(self):
- self.DB = None
- self.cursor = None
- self.open_flag = False
- self.init_database()
-
- def init_database(self, path=None):
- global db_path
- if not self.open_flag:
- if path:
- db_path = path
- if os.path.exists(db_path):
- self.DB = sqlite3.connect(db_path, check_same_thread=False)
- # '''创建游标'''
- self.cursor = self.DB.cursor()
- self.open_flag = True
- if lock.locked():
- lock.release()
-
- def add_sender(self, messages):
- """
- @param messages:
- @return:
- """
- new_messages = []
- for message in messages:
- is_sender = message[4]
- wxid = ''
- if is_sender:
- pass
- else:
- msgbytes = MessageBytesExtra()
- msgbytes.ParseFromString(message[10])
- for tmp in msgbytes.message2:
- if tmp.field1 != 1:
- continue
- wxid = tmp.field2
- new_message = (*message, wxid)
- new_messages.append(new_message)
- return new_messages
-
- def get_messages(
- self,
- username_,
- time_range: Tuple[int | float | str | date, int | float | str | date] = None,
- ):
- """
- return list
- a[0]: localId,
- a[1]: talkerId, (和strtalker对应的,不是群聊信息发送人)
- a[2]: type,
- a[3]: subType,
- a[4]: is_sender,
- a[5]: timestamp,
- a[6]: status, (没啥用)
- a[7]: str_content,
- a[8]: str_time, (格式化的时间)
- a[9]: msgSvrId,
- a[10]: BytesExtra,
- a[11]: CompressContent,
- a[12]: DisplayContent,
- a[13]: 联系人的类(如果是群聊就有,不是的话没有这个字段)
- """
- if not self.open_flag:
- return None
- if time_range:
- start_time, end_time = convert_to_timestamp(time_range)
- sql = f'''
- select localId,TalkerId,Type,SubType,IsSender,CreateTime,Status,StrContent,strftime('%Y-%m-%d %H:%M:%S',CreateTime,'unixepoch','localtime') as StrTime,MsgSvrID,BytesExtra,CompressContent,DisplayContent
- from MSG
- where StrTalker=?
- {'AND CreateTime>' + str(start_time) + ' AND CreateTime<' + str(end_time) if time_range else ''}
- order by CreateTime
- '''
- try:
- lock.acquire(True)
- self.cursor.execute(sql, [username_])
- result = self.cursor.fetchall()
- finally:
- lock.release()
- return parser_chatroom_message(result) if username_.__contains__('@chatroom') else result
- # result.sort(key=lambda x: x[5])
- # return self.add_sender(result)
-
- def get_messages_all(self, time_range=None):
- if time_range:
- start_time, end_time = convert_to_timestamp(time_range)
- sql = f'''
- select localId,TalkerId,Type,SubType,IsSender,CreateTime,Status,StrContent,strftime('%Y-%m-%d %H:%M:%S',CreateTime,'unixepoch','localtime') as StrTime,MsgSvrID,BytesExtra,StrTalker,Reserved1,CompressContent
- from MSG
- {'WHERE CreateTime>' + str(start_time) + ' AND CreateTime<' + str(end_time) if time_range else ''}
- order by CreateTime
- '''
- if not self.open_flag:
- return None
- try:
- lock.acquire(True)
- self.cursor.execute(sql)
- result = self.cursor.fetchall()
- finally:
- lock.release()
- result.sort(key=lambda x: x[5])
- return result
-
- def get_messages_group_by_day(
- self,
- username_: str,
- time_range: Tuple[int | float | str | date, int | float | str | date] = None,
-
- ) -> dict:
- """
- return dict {
- date: messages
- }
- """
- if not self.open_flag:
- return {}
- if time_range:
- start_time, end_time = convert_to_timestamp(time_range)
- sql = f'''
- select localId,TalkerId,Type,SubType,IsSender,CreateTime,Status,StrContent,strftime('%Y-%m-%d %H:%M:%S',CreateTime,'unixepoch','localtime') as StrTime,MsgSvrID,BytesExtra,CompressContent,DisplayContent
- from MSG
- where StrTalker=? AND type=1
- {'AND CreateTime>' + str(start_time) + ' AND CreateTime<' + str(end_time) if time_range else ''}
- order by CreateTime;
- '''
- try:
- lock.acquire(True)
- self.cursor.execute(sql, [username_])
- result = self.cursor.fetchall()
- finally:
- lock.release()
- result = parser_chatroom_message(result) if username_.__contains__('@chatroom') else result
-
- # 按天分组存储聊天记录
- grouped_results = defaultdict(list)
- for row in result:
- '2024-01-01'
- date = row[8][:10] # 获取日期部分
- grouped_results[date].append(row) # 将消息加入对应的日期列表中
-
- return grouped_results
-
- def get_messages_length(self):
- sql = '''
- select count(*)
- group by MsgSvrID
- from MSG
- '''
- if not self.open_flag:
- return None
- try:
- lock.acquire(True)
- self.cursor.execute(sql)
- result = self.cursor.fetchone()
- except Exception as e:
- result = None
- finally:
- lock.release()
- return result[0]
-
- def get_message_by_num(self, username_, local_id):
- sql = '''
- select localId,TalkerId,Type,SubType,IsSender,CreateTime,Status,StrContent,strftime('%Y-%m-%d %H:%M:%S',CreateTime,'unixepoch','localtime') as StrTime,MsgSvrID,BytesExtra,CompressContent,DisplayContent
- from MSG
- where StrTalker = ? and localId < ? and (Type=1 or Type=3)
- order by CreateTime desc
- limit 20
- '''
- result = None
- if not self.open_flag:
- return None
- try:
- lock.acquire(True)
- self.cursor.execute(sql, [username_, local_id])
- result = self.cursor.fetchall()
- except sqlite3.DatabaseError:
- logger.error(f'{traceback.format_exc()}\n数据库损坏请删除msg文件夹重试')
- finally:
- lock.release()
- # result.sort(key=lambda x: x[5])
- return parser_chatroom_message(result) if username_.__contains__('@chatroom') else result
-
- def get_messages_by_type(
- self,
- username_,
- type_,
- year_='all',
- time_range: Tuple[int | float | str | date, int | float | str | date] = None,
- ):
- """
- @param username_:
- @param type_:
- @param year_:
- @param time_range: Tuple(timestamp:开始时间戳,timestamp:结束时间戳)
- @return:
- """
- if not self.open_flag:
- return None
- if time_range:
- start_time, end_time = convert_to_timestamp(time_range)
- if year_ == 'all':
- sql = f'''
- select localId,TalkerId,Type,SubType,IsSender,CreateTime,Status,StrContent,strftime('%Y-%m-%d %H:%M:%S',CreateTime,'unixepoch','localtime') as StrTime,MsgSvrID,BytesExtra,CompressContent,DisplayContent
- from MSG
- where StrTalker=? and Type=?
- {'AND CreateTime>' + str(start_time) + ' AND CreateTime<' + str(end_time) if time_range else ''}
- order by CreateTime
- '''
- try:
- lock.acquire(True)
- self.cursor.execute(sql, [username_, type_])
- result = self.cursor.fetchall()
- finally:
- lock.release()
- else:
- sql = '''
- select localId,TalkerId,Type,SubType,IsSender,CreateTime,Status,StrContent,strftime('%Y-%m-%d %H:%M:%S',CreateTime,'unixepoch','localtime') as StrTime,MsgSvrID,BytesExtra,CompressContent,DisplayContent
- from MSG
- where StrTalker=? and Type=? and strftime('%Y', CreateTime, 'unixepoch', 'localtime') = ?
- order by CreateTime
- '''
- try:
- lock.acquire(True)
- self.cursor.execute(sql, [username_, type_, year_])
- finally:
- lock.release()
- result = self.cursor.fetchall()
- return result
-
- def get_messages_by_keyword(self, username_, keyword, num=5, max_len=10, time_range=None, year_='all'):
- if not self.open_flag:
- return None
- if time_range:
- start_time, end_time = convert_to_timestamp(time_range)
- sql = f'''
- select localId,TalkerId,Type,SubType,IsSender,CreateTime,Status,StrContent,strftime('%Y-%m-%d %H:%M:%S',CreateTime,'unixepoch','localtime') as StrTime,MsgSvrID,BytesExtra
- from MSG
- where StrTalker=? and Type=1 and LENGTH(StrContent) and StrContent like ?
- {'AND CreateTime>' + str(start_time) + ' AND CreateTime<' + str(end_time) if time_range else ''}
- order by CreateTime desc
- '''
- temp = []
- try:
- lock.acquire(True)
- self.cursor.execute(sql, [username_, max_len, f'%{keyword}%'] if year_ == "all" else [username_, max_len,
- f'%{keyword}%',
- year_])
- messages = self.cursor.fetchall()
- finally:
- lock.release()
- if len(messages) > 5:
- messages = random.sample(messages, num)
- try:
- lock.acquire(True)
- for msg in messages:
- local_id = msg[0]
- is_send = msg[4]
- sql = '''
- select localId,TalkerId,Type,SubType,IsSender,CreateTime,Status,StrContent,strftime('%Y-%m-%d %H:%M:%S',CreateTime,'unixepoch','localtime') as StrTime,MsgSvrID
- from MSG
- where localId > ? and StrTalker=? and Type=1 and IsSender=?
- limit 1
- '''
- self.cursor.execute(sql, [local_id, username_, 1 - is_send])
- temp.append((msg, self.cursor.fetchone()))
- finally:
- lock.release()
- res = []
- for dialog in temp:
- msg1 = dialog[0]
- msg2 = dialog[1]
- try:
- res.append((
- (msg1[4], msg1[5], msg1[7].split(keyword), msg1[8]),
- (msg2[4], msg2[5], msg2[7], msg2[8])
- ))
- except TypeError:
- res.append((
- ('', '', ['', ''], ''),
- ('', '', '', '')
- ))
- """
- 返回值为一个列表,每个列表元素是一个对话
- 每个对话是一个元组数据
- ('is_send','时间戳','以关键词为分割符的消息内容','格式化时间')
- """
- return res
-
- def get_contact(self, contacts):
- if not self.open_flag:
- return None
- try:
- lock.acquire(True)
- sql = '''select StrTalker, MAX(CreateTime) from MSG group by StrTalker'''
- self.cursor.execute(sql)
- res = self.cursor.fetchall()
- finally:
- lock.release()
- res = {StrTalker: CreateTime for StrTalker, CreateTime in res}
- contacts = [list(cur_contact) for cur_contact in contacts]
- for i, cur_contact in enumerate(contacts):
- if cur_contact[0] in res:
- contacts[i].append(res[cur_contact[0]])
- else:
- contacts[i].append(0)
- contacts.sort(key=lambda cur_contact: cur_contact[-1], reverse=True)
- return contacts
-
- def get_messages_calendar(self, username_):
- sql = '''
- SELECT strftime('%Y-%m-%d',CreateTime,'unixepoch','localtime') as days
- from (
- SELECT MsgSvrID, CreateTime
- FROM MSG
- WHERE StrTalker = ?
- ORDER BY CreateTime
- )
- group by days
- '''
- if not self.open_flag:
- print('数据库未就绪')
- return None
- try:
- lock.acquire(True)
- self.cursor.execute(sql, [username_])
- result = self.cursor.fetchall()
- finally:
- lock.release()
- return [date[0] for date in result]
-
- def get_messages_by_days(
- self,
- username_,
- time_range: Tuple[int | float | str | date, int | float | str | date] = None,
- ):
- result = None
- if not self.open_flag:
- return None
- if time_range:
- start_time, end_time = convert_to_timestamp(time_range)
- sql = f'''
- SELECT strftime('%Y-%m-%d',CreateTime,'unixepoch','localtime') as days,count(MsgSvrID)
- from (
- SELECT MsgSvrID, CreateTime
- FROM MSG
- WHERE StrTalker = ?
- {'AND CreateTime>' + str(start_time) + ' AND CreateTime<' + str(end_time) if time_range else ''}
- )
- group by days
- '''
- result = None
- if not self.open_flag:
- return None
- try:
- lock.acquire(True)
- self.cursor.execute(sql, [username_])
- result = self.cursor.fetchall()
- finally:
- lock.release()
- return result
-
- def get_messages_by_month(
- self,
- username_,
- time_range: Tuple[int | float | str | date, int | float | str | date] = None,
- ):
- result = None
- if not self.open_flag:
- return None
- if time_range:
- start_time, end_time = convert_to_timestamp(time_range)
- sql = f'''
- SELECT strftime('%Y-%m',CreateTime,'unixepoch','localtime') as days,count(MsgSvrID)
- from (
- SELECT MsgSvrID, CreateTime
- FROM MSG
- WHERE StrTalker = ?
- {'AND CreateTime>' + str(start_time) + ' AND CreateTime<' + str(end_time) if time_range else ''}
- )
- group by days
- '''
- try:
- lock.acquire(True)
- self.cursor.execute(sql, [username_])
- result = self.cursor.fetchall()
- except sqlite3.DatabaseError:
- logger.error(f'{traceback.format_exc()}\n数据库损坏请删除msg文件夹重试')
- finally:
- lock.release()
- return result
-
- def get_messages_by_hour(self, username_, time_range=None, year_='all'):
- result = []
- if not self.open_flag:
- return result
- if time_range:
- start_time, end_time = convert_to_timestamp(time_range)
- sql = f'''
- SELECT strftime('%H:00',CreateTime,'unixepoch','localtime') as hours,count(MsgSvrID)
- from (
- SELECT MsgSvrID, CreateTime
- FROM MSG
- where StrTalker = ?
- {'AND CreateTime>' + str(start_time) + ' AND CreateTime<' + str(end_time) if time_range else ''}
- )
- group by hours
- '''
- try:
- lock.acquire(True)
- self.cursor.execute(sql, [username_])
- except sqlite3.DatabaseError:
- logger.error(f'{traceback.format_exc()}\n数据库损坏请删除msg文件夹重试')
- finally:
- lock.release()
- result = self.cursor.fetchall()
- return result
-
- def get_first_time_of_message(self, username_=''):
- if not self.open_flag:
- return None
- sql = f'''
- select StrContent,strftime('%Y-%m-%d %H:%M:%S',CreateTime,'unixepoch','localtime') as StrTime
- from MSG
- {'where StrTalker=?' if username_ else ''}
- order by CreateTime
- limit 1
- '''
- try:
- lock.acquire(True)
- self.cursor.execute(sql, [username_] if username_ else [])
- result = self.cursor.fetchone()
- finally:
- lock.release()
- return result
-
- def get_latest_time_of_message(self, username_='', time_range=None, year_='all'):
- if not self.open_flag:
- return None
- if time_range:
- start_time, end_time = convert_to_timestamp(time_range)
- sql = f'''
- SELECT isSender,StrContent,strftime('%Y-%m-%d %H:%M:%S',CreateTime,'unixepoch','localtime') as StrTime,
- strftime('%H:%M:%S', CreateTime,'unixepoch','localtime') as hour
- FROM MSG
- WHERE Type=1 AND
- {'StrTalker = ? AND ' if username_ else f"'{username_}'=? AND "}
- hour BETWEEN '00:00:00' AND '05:00:00'
- {'AND CreateTime>' + str(start_time) + ' AND CreateTime<' + str(end_time) if time_range else ''}
- ORDER BY hour DESC
- LIMIT 20;
- '''
- try:
- lock.acquire(True)
- self.cursor.execute(sql, [username_, year_] if year_ != "all" else [username_])
- except sqlite3.DatabaseError:
- logger.error(f'{traceback.format_exc()}\n数据库损坏请删除msg文件夹重试')
- finally:
- lock.release()
- result = self.cursor.fetchall()
- if not result:
- return []
- res = []
- is_sender = result[0][0]
- res.append(result[0])
- for msg in result[1:]:
- if msg[0] != is_sender:
- res.append(msg)
- break
- return res
-
- def get_send_messages_type_number(
- self,
- time_range: Tuple[int | float | str | date, int | float | str | date] = None,
- ) -> list:
- """
- 统计自己发的各类型消息条数,按条数降序,精确到subtype\n
- return [(type_1, subtype_1, number_1), (type_2, subtype_2, number_2), ...]\n
- be like [(1, 0, 71481), (3, 0, 6686), (49, 57, 3887), ..., (10002, 0, 1)]
- """
- if time_range:
- start_time, end_time = convert_to_timestamp(time_range)
- sql = f"""
- SELECT type, subtype, Count(MsgSvrID)
- from MSG
- where isSender = 1
- {'AND CreateTime>' + str(start_time) + ' AND CreateTime<' + str(end_time) if time_range else ''}
- group by type, subtype
- order by Count(MsgSvrID) desc
- """
- result = None
- if not self.open_flag:
- return None
- try:
- lock.acquire(True)
- self.cursor.execute(sql)
- result = self.cursor.fetchall()
- except sqlite3.DatabaseError:
- logger.error(f'{traceback.format_exc()}\n数据库损坏请删除msg文件夹重试')
- finally:
- lock.release()
- return result
-
- def get_messages_number(
- self,
- username_,
- time_range: Tuple[int | float | str | date, int | float | str | date] = None,
- ) -> int:
- """
- 统计好友聊天消息的数量
- @param username_:
- @param time_range:
- @return:
- """
- if time_range:
- start_time, end_time = convert_to_timestamp(time_range)
- sql = f"""
- SELECT Count(MsgSvrID)
- from MSG
- where StrTalker = ?
- {'AND CreateTime>' + str(start_time) + ' AND CreateTime<' + str(end_time) if time_range else ''}
- """
- result = 0
- if not self.open_flag:
- return 0
- try:
- lock.acquire(True)
- self.cursor.execute(sql, [username_])
- result = self.cursor.fetchone()
- except sqlite3.DatabaseError:
- logger.error(f'{traceback.format_exc()}\n数据库损坏请删除msg文件夹重试')
- finally:
- lock.release()
- return result[0] if result else 0
-
- def get_chatted_top_contacts(
- self,
- time_range: Tuple[int | float | str | date, int | float | str | date] = None,
- contain_chatroom=False,
- top_n=10
- ) -> list:
- """
- 统计聊天最多的 n 个联系人(默认不包含群组),按条数降序\n
- return [(wxid_1, number_1), (wxid_2, number_2), ...]
- """
- if time_range:
- start_time, end_time = convert_to_timestamp(time_range)
- sql = f"""
- SELECT strtalker, Count(MsgSvrID)
- from MSG
- where strtalker != "filehelper" and strtalker != "notifymessage" and strtalker not like "gh_%"
- {"and strtalker not like '%@chatroom'" if not contain_chatroom else ""}
- {'AND CreateTime>' + str(start_time) + ' AND CreateTime<' + str(end_time) if time_range else ''}
- group by strtalker
- order by Count(MsgSvrID) desc
- limit {top_n}
- """
- result = None
- if not self.open_flag:
- return None
- try:
- lock.acquire(True)
- self.cursor.execute(sql)
- result = self.cursor.fetchall()
- except sqlite3.DatabaseError:
- logger.error(f'{traceback.format_exc()}\n数据库损坏请删除msg文件夹重试')
- finally:
- lock.release()
- return result
-
- def get_send_messages_length(
- self,
- time_range: Tuple[int | float | str | date, int | float | str | date] = None,
- ) -> int:
- """
- 统计自己总共发消息的字数,包含type=1的文本和type=49,subtype=57里面自己发的文本
- """
- if time_range:
- start_time, end_time = convert_to_timestamp(time_range)
- sql_type_1 = f"""
- SELECT sum(length(strContent))
- from MSG
- where isSender = 1 and type = 1
- {'AND CreateTime>' + str(start_time) + ' AND CreateTime<' + str(end_time) if time_range else ''}
- """
- sql_type_49 = f"""
- SELECT CompressContent
- from MSG
- where isSender = 1 and type = 49 and subtype = 57
- {'AND CreateTime>' + str(start_time) + ' AND CreateTime<' + str(end_time) if time_range else ''}
- """
- sum_type_1 = None
- result_type_49 = None
- sum_type_49 = 0
-
- if not self.open_flag:
- return None
- try:
- lock.acquire(True)
- self.cursor.execute(sql_type_1)
- sum_type_1 = self.cursor.fetchall()[0][0]
- self.cursor.execute(sql_type_49)
- result_type_49 = self.cursor.fetchall()
- for message in result_type_49:
- message = message[0]
- content = parser_reply(message)
- if content["is_error"]:
- continue
- sum_type_49 += len(content["title"])
- except sqlite3.DatabaseError:
- logger.error(f'{traceback.format_exc()}\n数据库损坏请删除msg文件夹重试')
- finally:
- lock.release()
- return sum_type_1 + sum_type_49
-
- def get_send_messages_number_sum(
- self,
- time_range: Tuple[int | float | str | date, int | float | str | date] = None,
- ) -> int:
- """统计自己总共发了多少条消息"""
- if time_range:
- start_time, end_time = convert_to_timestamp(time_range)
- sql = f"""
- SELECT count(MsgSvrID)
- from MSG
- where isSender = 1
- {'AND CreateTime>' + str(start_time) + ' AND CreateTime<' + str(end_time) if time_range else ''}
- """
- result = None
- if not self.open_flag:
- return None
- try:
- lock.acquire(True)
- self.cursor.execute(sql)
- result = self.cursor.fetchall()[0][0]
- except sqlite3.DatabaseError:
- logger.error(f'{traceback.format_exc()}\n数据库损坏请删除msg文件夹重试')
- finally:
- lock.release()
- return result
-
- def get_send_messages_number_by_hour(
- self,
- time_range: Tuple[int | float | str | date, int | float | str | date] = None,
- ) -> list:
- """
- 统计每个(小时)时段自己总共发了多少消息,从最多到最少排序\n
- return be like [('23', 9526), ('00', 7890), ('22', 7600), ..., ('05', 29)]
- """
- if time_range:
- start_time, end_time = convert_to_timestamp(time_range)
- sql = f"""
- SELECT strftime('%H', CreateTime, 'unixepoch', 'localtime') as hour,count(MsgSvrID)
- from (
- SELECT MsgSvrID, CreateTime
- FROM MSG
- where isSender = 1
- {'AND CreateTime>' + str(start_time) + ' AND CreateTime<' + str(end_time) if time_range else ''}
- )
- group by hour
- order by count(MsgSvrID) desc
- """
- result = None
- if not self.open_flag:
- return None
- try:
- lock.acquire(True)
- self.cursor.execute(sql)
- result = self.cursor.fetchall()
- except sqlite3.DatabaseError:
- logger.error(f'{traceback.format_exc()}\n数据库损坏请删除msg文件夹重试')
- finally:
- lock.release()
- return result
-
- def get_message_length(
- self,
- username_='',
- time_range: Tuple[int | float | str | date, int | float | str | date] = None,
- ) -> int:
- """
- 统计自己总共发消息的字数,包含type=1的文本和type=49,subtype=57里面自己发的文本
- """
- if time_range:
- start_time, end_time = convert_to_timestamp(time_range)
- sql_type_1 = f"""
- SELECT sum(length(strContent))
- from MSG
- where StrTalker = ? and
- type = 1
- {'AND CreateTime>' + str(start_time) + ' AND CreateTime<' + str(end_time) if time_range else ''}
- """
- sql_type_49 = f"""
- SELECT CompressContent
- from MSG
- where StrTalker = ? and
- type = 49 and subtype = 57
- {'AND CreateTime>' + str(start_time) + ' AND CreateTime<' + str(end_time) if time_range else ''}
- """
- sum_type_1 = 0
- result_type_1 = 0
- result_type_49 = 0
- sum_type_49 = 0
-
- if not self.open_flag:
- return None
- try:
- lock.acquire(True)
- self.cursor.execute(sql_type_1, [username_])
- result_type_1 = self.cursor.fetchall()[0][0]
- self.cursor.execute(sql_type_49, [username_])
- result_type_49 = self.cursor.fetchall()
- except sqlite3.DatabaseError:
- logger.error(f'{traceback.format_exc()}\n数据库损坏请删除msg文件夹重试')
- finally:
- lock.release()
- for message in result_type_49:
- message = message[0]
- content = parser_reply(message)
- if content["is_error"]:
- continue
- sum_type_49 += len(content["title"])
- sum_type_1 = result_type_1 if result_type_1 else 0
- return sum_type_1 + sum_type_49
-
- def close(self):
- if self.open_flag:
- try:
- lock.acquire(True)
- self.open_flag = False
- self.DB.close()
- finally:
- lock.release()
-
- def __del__(self):
- self.close()
-
-
-if __name__ == '__main__':
- db_path = "./Msg/MSG.db"
- msg = Msg()
- msg.init_database()
- wxid = 'wxid_0o18ef858vnu22'
- wxid = '24521163022@chatroom'
- wxid = 'wxid_vtz9jk9ulzjt22' # si
- print()
- time_range = ('2023-01-01 00:00:00', '2024-01-01 00:00:00')
- print(msg.get_messages_calendar(wxid))
- print(msg.get_first_time_of_message())
- print(msg.get_latest_time_of_message())
- top_n = msg.get_chatted_top_contacts(time_range=time_range, top_n=9999999)
- print(top_n)
- print(len(top_n))
diff --git a/app/DataBase/package_msg.py b/app/DataBase/package_msg.py
deleted file mode 100644
index ded0486..0000000
--- a/app/DataBase/package_msg.py
+++ /dev/null
@@ -1,184 +0,0 @@
-import threading
-
-from app.DataBase import msg_db, micro_msg_db, misc_db
-from app.util.protocbuf.msg_pb2 import MessageBytesExtra
-from app.util.protocbuf.roomdata_pb2 import ChatRoomData
-from app.person import Contact, Me, ContactDefault
-
-lock = threading.Lock()
-
-
-def singleton(cls):
- _instance = {}
-
- def inner():
- if cls not in _instance:
- _instance[cls] = cls()
- return _instance[cls]
-
- return inner
-
-
-@singleton
-class PackageMsg:
- def __init__(self):
- self.ChatRoomMap = {}
-
- def get_package_message_all(self):
- '''
- 获取完整的聊天记录
- '''
- updated_messages = [] # 用于存储修改后的消息列表
-
- messages = msg_db.get_messages_all()
- for row in messages:
- row_list = list(row)
- # 删除不使用的几个字段
- del row_list[13]
- del row_list[12]
- del row_list[11]
- del row_list[10]
- del row_list[9]
-
- strtalker = row[11]
- info = micro_msg_db.get_contact_by_username(strtalker)
- if info is not None:
- row_list.append(info[3])
- row_list.append(info[4])
- else:
- row_list.append('')
- row_list.append('')
- # 判断是否是群聊
- if strtalker.__contains__('@chatroom'):
- # 自己发送
- if row[4] == 1:
- row_list.append('我')
- else:
- # 存在BytesExtra为空的情况,此时消息类型应该为提示性消息。跳过不处理
- if row[10] is None:
- continue
- # 解析BytesExtra
- msgbytes = MessageBytesExtra()
- msgbytes.ParseFromString(row[10])
- wxid = ''
- for tmp in msgbytes.message2:
- if tmp.field1 != 1:
- continue
- wxid = tmp.field2
- sender = ''
- # 获取群聊成员列表
- membersMap = self.get_chatroom_member_list(strtalker)
- if membersMap is not None:
- if wxid in membersMap:
- sender = membersMap.get(wxid)
- else:
- senderinfo = micro_msg_db.get_contact_by_username(wxid)
- if senderinfo is not None:
- sender = senderinfo[4]
- membersMap[wxid] = senderinfo[4]
- if len(senderinfo[3]) > 0:
- sender = senderinfo[3]
- membersMap[wxid] = senderinfo[3]
- row_list.append(sender)
- else:
- if row[4] == 1:
- row_list.append('我')
- else:
- if info is not None:
- row_list.append(info[4])
- else:
- row_list.append('')
- updated_messages.append(tuple(row_list))
- return updated_messages
-
- def get_package_message_by_wxid(self, chatroom_wxid):
- '''
- 获取一个群聊的聊天记录
- return list
- a[0]: localId,
- a[1]: talkerId, (和strtalker对应的,不是群聊信息发送人)
- a[2]: type,
- a[3]: subType,
- a[4]: is_sender,
- a[5]: timestamp,
- a[6]: status, (没啥用)
- a[7]: str_content,
- a[8]: str_time, (格式化的时间)
- a[9]: msgSvrId,
- a[10]: BytesExtra,
- a[11]: CompressContent,
- a[12]: DisplayContent,
- a[13]: msg_sender, (ContactPC 或 ContactDefault 类型,这个才是群聊里的信息发送人,不是群聊或者自己是发送者没有这个字段)
- '''
- updated_messages = [] # 用于存储修改后的消息列表
- messages = msg_db.get_messages(chatroom_wxid)
- for row in messages:
- message = list(row)
- if message[4] == 1: # 自己发送的就没必要解析了
- message.append(Me())
- updated_messages.append(message)
- continue
- if message[10] is None: # BytesExtra是空的跳过
- message.append(ContactDefault(wxid))
- updated_messages.append(message)
- continue
- msgbytes = MessageBytesExtra()
- msgbytes.ParseFromString(message[10])
- wxid = ''
- for tmp in msgbytes.message2:
- if tmp.field1 != 1:
- continue
- wxid = tmp.field2
- if wxid == "": # 系统消息里面 wxid 不存在
- message.append(ContactDefault(wxid))
- updated_messages.append(message)
- continue
- contact_info_list = micro_msg_db.get_contact_by_username(wxid)
- if contact_info_list is None: # 群聊中已退群的联系人不会保存在数据库里
- message.append(ContactDefault(wxid))
- updated_messages.append(message)
- continue
- contact_info = {
- 'UserName': contact_info_list[0],
- 'Alias': contact_info_list[1],
- 'Type': contact_info_list[2],
- 'Remark': contact_info_list[3],
- 'NickName': contact_info_list[4],
- 'smallHeadImgUrl': contact_info_list[7]
- }
- contact = Contact(contact_info)
- contact.smallHeadImgBLOG = misc_db.get_avatar_buffer(contact.wxid)
- contact.set_avatar(contact.smallHeadImgBLOG)
- message.append(contact)
- updated_messages.append(tuple(message))
- return updated_messages
-
- def get_chatroom_member_list(self, strtalker):
- membermap = {}
- '''
- 获取群聊成员
- '''
- try:
- lock.acquire(True)
- if strtalker in self.ChatRoomMap:
- membermap = self.ChatRoomMap.get(strtalker)
- else:
- chatroom = micro_msg_db.get_chatroom_info(strtalker)
- if chatroom is None:
- return None
- # 解析RoomData数据
- parsechatroom = ChatRoomData()
- parsechatroom.ParseFromString(chatroom[1])
- # 群成员数据放入字典存储
- for mem in parsechatroom.members:
- if mem.displayName is not None and len(mem.displayName) > 0:
- membermap[mem.wxID] = mem.displayName
- self.ChatRoomMap[strtalker] = membermap
- finally:
- lock.release()
- return membermap
-
-
-if __name__ == "__main__":
- p = PackageMsg()
- print(p.get_package_message_by_wxid("48615079469@chatroom"))
diff --git a/app/__init__.py b/app/__init__.py
deleted file mode 100644
index 2def13e..0000000
--- a/app/__init__.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-@File : __init__.py.py
-@Author : Shuaikang Zhou
-@Time : 2023/1/5 17:43
-@IDE : Pycharm
-@Version : Python3.10
-@comment : ···
-"""
diff --git a/app/analysis/analysis.py b/app/analysis/analysis.py
deleted file mode 100644
index 2690776..0000000
--- a/app/analysis/analysis.py
+++ /dev/null
@@ -1,535 +0,0 @@
-import os
-from collections import Counter
-import sys
-from datetime import datetime
-from typing import List
-
-import jieba
-
-from app.DataBase import msg_db, MsgType
-from pyecharts import options as opts
-from pyecharts.charts import WordCloud, Calendar, Bar, Line, Pie, Map
-
-from app.person import Contact
-from app.util.region_conversion import conversion_province_to_chinese
-
-os.makedirs('./data/聊天统计/', exist_ok=True)
-
-
-def wordcloud_(wxid, time_range=None):
- import jieba
- txt_messages = msg_db.get_messages_by_type(wxid, MsgType.TEXT, time_range=time_range)
- if not txt_messages:
- return {
- 'chart_data': None,
- 'keyword': "没有聊天你想分析啥",
- 'max_num': "0",
- 'dialogs': []
- }
- # text = ''.join(map(lambda x: x[7], txt_messages))
- text = ''.join(map(lambda x: x[7], txt_messages)) # 1“我”说的话,0“Ta”说的话
-
- total_msg_len = len(text)
- # 使用jieba进行分词,并加入停用词
- words = jieba.cut(text)
- # 统计词频
- word_count = Counter(words)
- # 过滤停用词
- stopwords_file = './app/data/stopwords.txt'
- with open(stopwords_file, "r", encoding="utf-8") as stopword_file:
- stopwords1 = set(stopword_file.read().splitlines())
- # 构建 FFmpeg 可执行文件的路径
- stopwords = set()
- stopwords_file = './app/resources/data/stopwords.txt'
- if not os.path.exists(stopwords_file):
- resource_dir = getattr(sys, '_MEIPASS', os.path.abspath(os.path.dirname(__file__)))
- stopwords_file = os.path.join(resource_dir, 'app', 'resources', 'data', 'stopwords.txt')
- with open(stopwords_file, "r", encoding="utf-8") as stopword_file:
- stopwords = set(stopword_file.read().splitlines())
- stopwords = stopwords.union(stopwords1)
- filtered_word_count = {word: count for word, count in word_count.items() if len(word) > 1 and word not in stopwords}
-
- # 转换为词云数据格式
- data = [(word, count) for word, count in filtered_word_count.items()]
- # text_data = data
- data.sort(key=lambda x: x[1], reverse=True)
-
- text_data = data[:100] if len(data) > 100 else data
- # 创建词云图
- keyword, max_num = text_data[0]
- w = (
- WordCloud(init_opts=opts.InitOpts())
- .add(series_name="聊天文字", data_pair=text_data, word_size_range=[5, 100])
- )
- # return w.render_embed()
- return {
- 'chart_data': w.dump_options_with_quotes(),
- 'keyword': keyword,
- 'max_num': str(max_num),
- 'dialogs': msg_db.get_messages_by_keyword(wxid, keyword, num=5, max_len=12)
- }
-
-
-def get_wordcloud(text):
- total_msg_len = len(text)
- jieba.load_userdict('./app/data/new_words.txt')
- # 使用jieba进行分词,并加入停用词
- words = jieba.cut(text)
- # 统计词频
- word_count = Counter(words)
- # 过滤停用词
- stopwords_file = './app/data/stopwords.txt'
- with open(stopwords_file, "r", encoding="utf-8") as stopword_file:
- stopwords1 = set(stopword_file.read().splitlines())
- # 构建 FFmpeg 可执行文件的路径
- stopwords = set()
- stopwords_file = './app/resources/data/stopwords.txt'
- if not os.path.exists(stopwords_file):
- resource_dir = getattr(sys, '_MEIPASS', os.path.abspath(os.path.dirname(__file__)))
- stopwords_file = os.path.join(resource_dir, 'app', 'resources', 'data', 'stopwords.txt')
- with open(stopwords_file, "r", encoding="utf-8") as stopword_file:
- stopwords = set(stopword_file.read().splitlines())
- stopwords = stopwords.union(stopwords1)
-
- filtered_word_count = {word: count for word, count in word_count.items() if len(word) > 1 and word not in stopwords}
- # 转换为词云数据格式
- data = [(word, count) for word, count in filtered_word_count.items()]
- # text_data = data
- data.sort(key=lambda x: x[1], reverse=True)
-
- text_data = data[:100] if len(data) > 100 else data
- # 创建词云图
- if text_data:
- keyword, max_num = text_data[0]
- else:
- keyword, max_num = '', 0
- w = (
- WordCloud()
- .add(series_name="聊天文字", data_pair=text_data, word_size_range=[5, 40])
- )
- return {
- 'chart_data_wordcloud': w.dump_options_with_quotes(),
- 'keyword': keyword,
- 'keyword_max_num': max_num,
- }
-
-
-def wordcloud_christmas(wxid,time_range=None, year='2023'):
- import jieba
-
- txt_messages = msg_db.get_messages_by_type(wxid, MsgType.TEXT, time_range=time_range)
- if not txt_messages:
- return {
- 'wordcloud_chart_data': None,
- 'keyword': "没有聊天你想分析啥",
- 'max_num': '0',
- 'dialogs': [],
- 'total_num': 0,
- }
- text = ''.join(map(lambda x: x[7], txt_messages))
- total_msg_len = len(text)
- wordcloud_data = get_wordcloud(text)
- # return w.render_embed()
- keyword = wordcloud_data.get('keyword')
- max_num = wordcloud_data.get('keyword_max_num')
- dialogs = msg_db.get_messages_by_keyword(wxid, keyword, num=3, max_len=12, time_range=time_range)
-
- return {
- 'wordcloud_chart_data': wordcloud_data.get('chart_data_wordcloud'),
- 'keyword': keyword,
- 'keyword_max_num': str(max_num),
- 'dialogs': dialogs,
- 'total_num': total_msg_len,
- }
-
-
-def calendar_chart(wxid, time_range=None):
- calendar_data = msg_db.get_messages_by_days(wxid, time_range)
- if not calendar_data:
- return {
- 'chart_data': None,
- 'calendar_chart_data': None,
- 'chat_days': 0,
- # 'chart':c,
- }
- min_ = min(map(lambda x: x[1], calendar_data))
- max_ = max(map(lambda x: x[1], calendar_data))
- start_date_ = calendar_data[0][0]
- end_date_ = calendar_data[-1][0]
- print(start_date_, '---->', end_date_)
- calendar_days = (start_date_, end_date_)
- calendar_title = '和Ta的聊天情况'
- c = (
- Calendar()
- .add(
- "",
- calendar_data,
- calendar_opts=opts.CalendarOpts(range_=calendar_days)
- )
- .set_global_opts(
- visualmap_opts=opts.VisualMapOpts(
- max_=max_,
- min_=min_,
- orient="horizontal",
- pos_bottom="0px",
- pos_left="0px",
- ),
- legend_opts=opts.LegendOpts(is_show=False)
- )
- )
- return {
- 'chart_data': c.dump_options_with_quotes(),
- 'calendar_chart_data': c.dump_options_with_quotes(),
- 'chat_days': len(calendar_data),
- # 'chart':c,
- }
-
-
-def month_count(wxid, time_range=None):
- """
- 每月聊天条数
- """
- msg_data = msg_db.get_messages_by_month(wxid, time_range)
- y_data = list(map(lambda x: x[1], msg_data))
- x_axis = list(map(lambda x: x[0], msg_data))
- m = (
- Bar(init_opts=opts.InitOpts())
- .add_xaxis(x_axis)
- .add_yaxis("消息数量", y_data,
- label_opts=opts.LabelOpts(is_show=True),
- itemstyle_opts=opts.ItemStyleOpts(color="#ffae80"),
- )
- .set_global_opts(
- title_opts=opts.TitleOpts(title="逐月统计", subtitle=None),
- datazoom_opts=opts.DataZoomOpts(),
- toolbox_opts=opts.ToolboxOpts(),
- yaxis_opts=opts.AxisOpts(
- name="消息数",
- type_="value",
- axistick_opts=opts.AxisTickOpts(is_show=True),
- splitline_opts=opts.SplitLineOpts(is_show=True),
- ),
- visualmap_opts=opts.VisualMapOpts(
- min_=min(y_data),
- max_=max(y_data),
- dimension=1, # 根据第2个维度(y 轴)进行映射
- is_piecewise=False, # 是否分段显示
- range_color=["#ffbe7a", "#fa7f6f"], # 设置颜色范围
- type_="color",
- pos_right="0%",
- ),
- )
- )
- return {
- 'chart_data': m.dump_options_with_quotes(),
- # 'chart': m,
- }
-
-
-def hour_count(wxid, is_Annual_report=False, year='2023'):
- """
- 小时计数聊天条数
- """
- msg_data = msg_db.get_messages_by_hour(wxid, is_Annual_report, year)
- print(msg_data)
- y_data = list(map(lambda x: x[1], msg_data))
- x_axis = list(map(lambda x: x[0], msg_data))
- h = (
- Line(init_opts=opts.InitOpts())
- .add_xaxis(xaxis_data=x_axis)
- .add_yaxis(
- series_name="聊天频率",
- y_axis=y_data,
- markpoint_opts=opts.MarkPointOpts(
- data=[
- opts.MarkPointItem(type_="max", name="最大值"),
- opts.MarkPointItem(type_="min", name="最小值", value=int(10)),
- ]
- ),
- markline_opts=opts.MarkLineOpts(
- data=[opts.MarkLineItem(type_="average", name="平均值")]
- ),
- )
- .set_global_opts(
- title_opts=opts.TitleOpts(title="聊天时段", subtitle=None),
- # datazoom_opts=opts.DataZoomOpts(),
- # toolbox_opts=opts.ToolboxOpts(),
- )
- .set_series_opts(
- label_opts=opts.LabelOpts(
- is_show=False
- )
- )
- )
-
- return {
- 'chart_data': h
- }
-
-
-types = {
- '文本': 1,
- '图片': 3,
- '语音': 34,
- '视频': 43,
- '表情包': 47,
- '音乐与音频': 4903,
- '文件': 4906,
- '分享卡片': 4905,
- '转账': 492000,
- '音视频通话': 50,
- '拍一拍等系统消息': 10000,
-}
-types_ = {
- 1: '文本',
- 3: '图片',
- 34: '语音',
- 43: '视频',
- 47: '表情包',
- 4957: '引用消息',
- 4903: '音乐与音频',
- 4906: '文件',
- 4905: '分享卡片',
- 492000: '转账',
- 50: '音视频通话',
- 10000: '拍一拍等系统消息',
-}
-
-
-def get_weekday(timestamp):
- # 将时间戳转换为日期时间对象
- dt_object = datetime.fromtimestamp(timestamp)
-
- # 获取星期几,0代表星期一,1代表星期二,以此类推
- weekday = dt_object.weekday()
- weekdays = ['周一', '周二', '周三', '周四', '周五', '周六', '周日']
- return weekdays[weekday]
-
-
-def sender(wxid, time_range, my_name='', ta_name=''):
- msg_data = msg_db.get_messages(wxid, time_range)
-
- types_count = {}
- send_num = 0 # 发送消息的数量
- weekday_count = {}
- for message in msg_data:
- type_ = message[2]
- is_sender = message[4]
- subType = message[3]
- timestamp = message[5]
- weekday = get_weekday(timestamp)
- str_time = message[8]
- send_num += is_sender
- type_ = f'{type_}{subType:0>2d}' if subType != 0 else type_
- type_ = int(type_)
- if type_ in types_count:
- types_count[type_] += 1
- else:
- types_count[type_] = 1
- if weekday in weekday_count:
- weekday_count[weekday] += 1
- else:
- weekday_count[weekday] = 1
- receive_num = len(msg_data) - send_num
- data = [[types_.get(key), value] for key, value in types_count.items() if key in types_]
- if not data:
- return {
- 'chart_data_sender': None,
- 'chart_data_types': None,
- 'chart_data_weekday': None,
- }
- p1 = (
- Pie()
- .add(
- "",
- data,
- center=["40%", "50%"],
- )
- .set_global_opts(
- datazoom_opts=opts.DataZoomOpts(),
- toolbox_opts=opts.ToolboxOpts(),
- title_opts=opts.TitleOpts(title="消息类型占比"),
- legend_opts=opts.LegendOpts(type_="scroll", pos_left="80%", pos_top="20%", orient="vertical"),
- )
- .set_series_opts(label_opts=opts.LabelOpts(formatter="{b}: {c}"))
- # .render("./data/聊天统计/types_pie.html")
- )
- p2 = (
- Pie()
- .add(
- "",
- [[my_name, send_num], [ta_name, receive_num]],
- center=["40%", "50%"],
- )
- .set_global_opts(
- datazoom_opts=opts.DataZoomOpts(),
- toolbox_opts=opts.ToolboxOpts(),
- title_opts=opts.TitleOpts(title="双方消息占比"),
- legend_opts=opts.LegendOpts(type_="scroll", pos_left="80%", pos_top="20%", orient="vertical"),
- )
- .set_series_opts(label_opts=opts.LabelOpts(formatter="{b}: {c}\n{d}%"))
- # .render("./data/聊天统计/pie_scroll_legend.html")
- )
- p3 = (
- Pie()
- .add(
- "",
- [[key, value] for key, value in weekday_count.items()],
- radius=["40%", "75%"],
- )
- .set_global_opts(
- datazoom_opts=opts.DataZoomOpts(),
- toolbox_opts=opts.ToolboxOpts(),
- title_opts=opts.TitleOpts(title="星期分布图"),
- legend_opts=opts.LegendOpts(orient="vertical", pos_top="15%", pos_left="2%"),
- )
- .set_series_opts(label_opts=opts.LabelOpts(formatter="{b}: {c}\n{d}%"))
- # .render("./data/聊天统计/pie_weekdays.html")
- )
- return {
- 'chart_data_sender': p2.dump_options_with_quotes(),
- 'chart_data_types': p1.dump_options_with_quotes(),
- 'chart_data_weekday': p3.dump_options_with_quotes(),
- }
-
-
-def contacts_analysis(contacts):
- man_contact_num = 0
- woman_contact_num = 0
- province_dict = {
- '北京': '北京市',
- '上海': '上海市',
- '天津': '天津市',
- '重庆': '重庆市',
- '新疆': '新疆维吾尔族自治区',
- '广西': '广西壮族自治区',
- '内蒙古': '内蒙古自治区',
- '宁夏': '宁夏回族自治区',
- '西藏': '西藏自治区'
- }
- provinces = []
- for contact, num, text_length in contacts:
- if contact.detail.get('gender') == 1:
- man_contact_num += 1
- elif contact.detail.get('gender') == 2:
- woman_contact_num += 1
- province_py = contact.detail.get('region')
- if province_py:
- province = province_py[1]
- province = conversion_province_to_chinese(province)
- if province:
- if province in province_dict:
- province = province_dict[province]
- else:
- province += '省'
- provinces.append(province)
- print(province, contact.detail)
- data = Counter(provinces)
- data = [[k, v] for k, v in data.items()]
- print(data)
- max_ = max(list(map(lambda x:x[1],data)))
- c = (
- Map()
- .add("分布", data, "china")
- .set_series_opts(label_opts=opts.LabelOpts(is_show=False))
- .set_global_opts(
- title_opts=opts.TitleOpts(title="地区分布"),
- visualmap_opts=opts.VisualMapOpts(max_=max_, is_piecewise=True),
- legend_opts=opts.LegendOpts(is_show=False),
- )
- )
- return {
- 'woman_contact_num': woman_contact_num,
- 'man_contact_num': man_contact_num,
- 'contact_region_map': c.dump_options_with_quotes(),
- }
-
-
-def my_message_counter(time_range, my_name=''):
- msg_data = msg_db.get_messages_all(time_range=time_range)
- types_count = {}
- send_num = 0 # 发送消息的数量
- weekday_count = {}
- str_content = ''
- total_text_num = 0
- for message in msg_data:
- type_ = message[2]
- is_sender = message[4]
- subType = message[3]
- timestamp = message[5]
- weekday = get_weekday(timestamp)
- str_time = message[8]
- send_num += is_sender
- type_ = f'{type_}{subType:0>2d}' if subType != 0 else type_
- type_ = int(type_)
- if type_ in types_count:
- types_count[type_] += 1
- else:
- types_count[type_] = 1
- if weekday in weekday_count:
- weekday_count[weekday] += 1
- else:
- weekday_count[weekday] = 1
- if type_ == 1:
- total_text_num += len(message[7])
- if is_sender == 1:
- str_content += message[7]
- receive_num = len(msg_data) - send_num
- data = [[types_.get(key), value] for key, value in types_count.items() if key in types_]
- if not data:
- return {
- 'chart_data_sender': None,
- 'chart_data_types': None,
- }
- p1 = (
- Pie()
- .add(
- "",
- data,
- center=["40%", "50%"],
- )
- .set_global_opts(
- datazoom_opts=opts.DataZoomOpts(),
- legend_opts=opts.LegendOpts(type_="scroll", pos_left="70%", pos_top="10%", orient="vertical"),
- )
- .set_series_opts(label_opts=opts.LabelOpts(formatter="{b}: {c}"))
- # .render("./data/聊天统计/types_pie.html")
- )
- p2 = (
- Pie()
- .add(
- "",
- [['发送', send_num], ['接收', receive_num]],
- center=["40%", "50%"],
- )
- .set_global_opts(
- datazoom_opts=opts.DataZoomOpts(),
- legend_opts=opts.LegendOpts(type_="scroll", pos_left="70%", pos_top="20%", orient="vertical"),
- )
- .set_series_opts(label_opts=opts.LabelOpts(formatter="{b}: {c}\n{d}%", position='inside'))
- # .render("./data/聊天统计/pie_scroll_legend.html")
- )
- w = get_wordcloud(str_content)
- return {
- 'chart_data_sender': p2.dump_options_with_quotes(),
- 'chart_data_types': p1.dump_options_with_quotes(),
- 'chart_data_wordcloud': w.get('chart_data_wordcloud'),
- 'keyword': w.get('keyword'),
- 'keyword_max_num': w.get('keyword_max_num'),
- 'total_text_num': total_text_num,
- }
-
-
-if __name__ == '__main__':
- msg_db.init_database(path='../DataBase/Msg/MSG.db')
- # w = wordcloud('wxid_0o18ef858vnu22')
- # w_data = wordcloud('wxid_27hqbq7vx5hf22', True, '2023')
- # # print(w_data)
- # w_data['chart_data'].render("./data/聊天统计/wordcloud.html")
- wxid = 'wxid_0o18ef858vnu22'
- # data = month_count(wxid, time_range=None)
- # data['chart'].render("./data/聊天统计/month_count.html")
- # data = calendar_chart(wxid, time_range=None)
- # data['chart'].render("./data/聊天统计/calendar_chart.html")
- data = sender(wxid, time_range=None, my_name='发送', ta_name='接收')
- print(data)
diff --git a/app/components/Button_Contact.py b/app/components/Button_Contact.py
deleted file mode 100644
index 8486d5a..0000000
--- a/app/components/Button_Contact.py
+++ /dev/null
@@ -1,103 +0,0 @@
-from datetime import datetime
-
-from PyQt5 import QtWidgets, QtGui, QtCore
-from PyQt5.QtCore import *
-
-from app import person
-
-
-class ContactUi(QtWidgets.QPushButton):
- """
- 联系人类,继承自pyqt的按钮,里面封装了联系人头像等标签
- """
- usernameSingal = pyqtSignal(str)
-
- def __init__(self, Ui, id=None, rconversation=None):
- super(ContactUi, self).__init__(Ui)
- self.contact: person.Contact = person.Contact(rconversation[1])
- self.init_ui(Ui)
- self.msgCount = rconversation[0]
- self.username = rconversation[1]
- self.conversationTime = rconversation[6]
- self.msgType = rconversation[7]
- self.digest = rconversation[8]
- hasTrunc = rconversation[10]
- attrflag = rconversation[11]
- if hasTrunc == 0:
- if attrflag == 0:
- self.digest = '[动画表情]'
- elif attrflag == 67108864:
- try:
- remark = data.get_conRemark(rconversation[9])
- msg = self.digest.split(':')[1].strip('\n').strip()
- self.digest = f'{remark}:{msg}'
- except Exception as e:
- pass
- else:
- pass
- self.show_info(id)
-
- def init_ui(self, Ui):
- self.layoutWidget = QtWidgets.QWidget(Ui)
- self.layoutWidget.setObjectName("layoutWidget")
- self.gridLayout1 = QtWidgets.QGridLayout(self.layoutWidget)
- self.gridLayout1.setSizeConstraint(QtWidgets.QLayout.SetMaximumSize)
- self.gridLayout1.setContentsMargins(10, 10, 10, 10)
- self.gridLayout1.setSpacing(10)
- self.gridLayout1.setObjectName("gridLayout1")
- self.label_time = QtWidgets.QLabel(self.layoutWidget)
- font = QtGui.QFont()
- font.setFamily("微软雅黑")
- font.setPointSize(8)
- self.label_time.setFont(font)
- self.label_time.setLayoutDirection(QtCore.Qt.RightToLeft)
- self.label_time.setAlignment(QtCore.Qt.AlignRight | QtCore.Qt.AlignTrailing | QtCore.Qt.AlignVCenter)
- self.label_time.setObjectName("label_time")
- self.gridLayout1.addWidget(self.label_time, 0, 2, 1, 1)
- self.label_remark = QtWidgets.QLabel(self.layoutWidget)
- font = QtGui.QFont()
- font.setFamily("黑体")
- font.setPointSize(10)
- # font.setBold(True)
- self.label_remark.setFont(font)
- self.label_remark.setObjectName("label_remark")
- self.gridLayout1.addWidget(self.label_remark, 0, 1, 1, 1)
- self.label_msg = QtWidgets.QLabel(self.layoutWidget)
- font = QtGui.QFont()
- font.setFamily("微软雅黑")
- font.setPointSize(8)
- self.label_msg.setFont(font)
- self.label_msg.setObjectName("label_msg")
- self.gridLayout1.addWidget(self.label_msg, 1, 1, 1, 2)
- self.label_avatar = QtWidgets.QLabel(self.layoutWidget)
- self.label_avatar.setMinimumSize(QtCore.QSize(60, 60))
- self.label_avatar.setMaximumSize(QtCore.QSize(60, 60))
- self.label_avatar.setLayoutDirection(QtCore.Qt.RightToLeft)
- self.label_avatar.setAutoFillBackground(False)
- self.label_avatar.setStyleSheet("background-color: #ffffff;")
- self.label_avatar.setInputMethodHints(QtCore.Qt.ImhNone)
- self.label_avatar.setFrameShape(QtWidgets.QFrame.NoFrame)
- self.label_avatar.setFrameShadow(QtWidgets.QFrame.Plain)
- self.label_avatar.setAlignment(QtCore.Qt.AlignLeading | QtCore.Qt.AlignLeft | QtCore.Qt.AlignVCenter)
- self.label_avatar.setObjectName("label_avatar")
- self.gridLayout1.addWidget(self.label_avatar, 0, 0, 2, 1)
- self.gridLayout1.setColumnStretch(0, 1)
- self.gridLayout1.setColumnStretch(1, 6)
- self.gridLayout1.setRowStretch(0, 5)
- self.gridLayout1.setRowStretch(1, 3)
- self.setLayout(self.gridLayout1)
- self.setStyleSheet(
- "QPushButton {background-color: rgb(220,220,220);}"
- "QPushButton:hover{background-color: rgb(208,208,208);}\n"
- )
-
- def show_info(self, id):
- time = datetime.now().strftime("%m-%d %H:%M")
- msg = '还没说话'
- self.label_avatar.setPixmap(self.contact.avatar) # 在label上显示图片
- self.label_remark.setText(self.contact.conRemark)
- self.label_msg.setText(self.digest)
- self.label_time.setText(data.timestamp2str(self.conversationTime)[2:])
-
- def show_msg(self):
- self.usernameSingal.emit(self.username)
diff --git a/app/components/CAvatar.py b/app/components/CAvatar.py
deleted file mode 100644
index 74c0ba7..0000000
--- a/app/components/CAvatar.py
+++ /dev/null
@@ -1,303 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Created on 2019年7月26日
-@author: Irony
-@site: https://pyqt5.com https://github.com/892768447
-@email: 892768447@qq.com
-@file: CustomWidgets.CAvatar
-@description: 头像
-"""
-import os
-
-from PyQt5.QtCore import QUrl, QRectF, Qt, QSize, QTimer, QPropertyAnimation, \
- QPointF, pyqtProperty
-from PyQt5.QtGui import QPixmap, QColor, QPainter, QPainterPath, QMovie
-from PyQt5.QtNetwork import QNetworkAccessManager, QNetworkDiskCache, \
- QNetworkRequest
-from PyQt5.QtWidgets import QWidget, qApp
-
-__Author__ = 'Irony'
-__Copyright__ = 'Copyright (c) 2019 Irony'
-__Version__ = 1.0
-
-
-class CAvatar(QWidget):
- Circle = 0 # 圆圈
- Rectangle = 1 # 圆角矩形
- SizeLarge = QSize(128, 128)
- SizeMedium = QSize(64, 64)
- SizeSmall = QSize(32, 32)
- StartAngle = 0 # 起始旋转角度
- EndAngle = 360 # 结束旋转角度
-
- def __init__(self, *args, shape=0, url='', img_bytes=None, cacheDir=False, size=QSize(64, 64), animation=False,
- **kwargs):
- super(CAvatar, self).__init__(*args, **kwargs)
- self.url = ''
- self._angle = 0 # 角度
- self.pradius = 0 # 加载进度条半径
- self.animation = animation # 是否使用动画
- self._movie = None # 动态图
- self._pixmap = QPixmap() # 图片对象
- self.pixmap = QPixmap() # 被绘制的对象
- self.isGif = url.endswith('.gif')
- # 进度动画定时器
- self.loadingTimer = QTimer(self, timeout=self.onLoading)
- # 旋转动画
- self.rotateAnimation = QPropertyAnimation(
- self, b'angle', self, loopCount=1)
- self.setShape(shape)
- self.setCacheDir(cacheDir)
- self.setSize(size)
- if img_bytes:
- self.setBytes(img_bytes)
-
-
- else:
- self.setUrl(url)
-
- def paintEvent(self, event):
- super(CAvatar, self).paintEvent(event)
- # 画笔
- painter = QPainter(self)
- painter.setRenderHint(QPainter.Antialiasing, True)
- painter.setRenderHint(QPainter.HighQualityAntialiasing, True)
- painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
- # 绘制
- path = QPainterPath()
- diameter = min(self.width(), self.height())
- if self.shape == self.Circle:
- radius = int(diameter / 2)
- elif self.shape == self.Rectangle:
- radius = 4
- halfW = self.width() / 2
- halfH = self.height() / 2
- painter.translate(halfW, halfH)
- path.addRoundedRect(
- QRectF(-halfW, -halfH, diameter, diameter), radius, radius)
- painter.setClipPath(path)
- # 如果是动画效果
- if self.rotateAnimation.state() == QPropertyAnimation.Running:
- painter.rotate(self._angle) # 旋转
- painter.drawPixmap(
- QPointF(-self.pixmap.width() / 2, -self.pixmap.height() // 2), self.pixmap)
- else:
- painter.drawPixmap(-int(halfW), -int(halfH), self.pixmap)
- # 如果在加载
- if self.loadingTimer.isActive():
- diameter = 2 * self.pradius
- painter.setBrush(
- QColor(45, 140, 240, int((1 - self.pradius / 10) * 255)))
- painter.setPen(Qt.NoPen)
- painter.drawRoundedRect(
- QRectF(-self.pradius, -self.pradius, diameter, diameter), self.pradius, self.pradius)
-
- def enterEvent(self, event):
- """鼠标进入动画
- :param event:
- """
- if not (self.animation and not self.isGif):
- return
- self.rotateAnimation.stop()
- cv = self.rotateAnimation.currentValue() or self.StartAngle
- self.rotateAnimation.setDuration(
- 540 if cv == 0 else int(cv / self.EndAngle * 540))
- self.rotateAnimation.setStartValue(cv)
- self.rotateAnimation.setEndValue(self.EndAngle)
- self.rotateAnimation.start()
-
- def leaveEvent(self, event):
- """鼠标离开动画
- :param event:
- """
- if not (self.animation and not self.isGif):
- return
- self.rotateAnimation.stop()
- cv = self.rotateAnimation.currentValue() or self.EndAngle
- self.rotateAnimation.setDuration(int(cv / self.EndAngle * 540))
- self.rotateAnimation.setStartValue(cv)
- self.rotateAnimation.setEndValue(self.StartAngle)
- self.rotateAnimation.start()
-
- def onLoading(self):
- """更新进度动画
- """
- if self.loadingTimer.isActive():
- if self.pradius > 9:
- self.pradius = 0
- self.pradius += 1
- else:
- self.pradius = 0
- self.update()
-
- def onFinished(self):
- """图片下载完成
- """
- self.loadingTimer.stop()
- self.pradius = 0
- reply = self.sender()
-
- if self.isGif:
- self._movie = QMovie(reply, b'gif', self)
- if self._movie.isValid():
- self._movie.frameChanged.connect(self._resizeGifPixmap)
- self._movie.start()
- else:
- data = reply.readAll().data()
- reply.deleteLater()
- del reply
- self._pixmap.loadFromData(data)
- if self._pixmap.isNull():
- self._pixmap = QPixmap(self.size())
- self._pixmap.fill(QColor(204, 204, 204))
- self._resizePixmap()
-
- def onError(self, code):
- """下载出错了
- :param code:
- """
- self._pixmap = QPixmap(self.size())
- self._pixmap.fill(QColor(204, 204, 204))
- self._resizePixmap()
-
- def refresh(self):
- """强制刷新
- """
- self._get(self.url)
-
- def isLoading(self):
- """判断是否正在加载
- """
- return self.loadingTimer.isActive()
-
- def setShape(self, shape):
- """设置形状
- :param shape: 0=圆形, 1=圆角矩形
- """
- self.shape = shape
-
- def setBytes(self, img_bytes):
- self._pixmap = QPixmap()
- if isinstance(img_bytes, bytes):
- if img_bytes[:4] == b'\x89PNG':
- self._pixmap.loadFromData(img_bytes, format='PNG')
- else:
- self._pixmap.loadFromData(img_bytes, format='jfif')
- elif isinstance(img_bytes, QPixmap):
- self._pixmap = img_bytes
- self._resizePixmap()
-
- def setUrl(self, url):
- """设置url,可以是本地路径,也可以是网络地址
- :param url:
- """
- return
- self.url = url
- self._get(url)
-
- def setCacheDir(self, cacheDir=''):
- """设置本地缓存路径
- :param cacheDir:
- """
- self.cacheDir = cacheDir
- self._initNetWork()
-
- def setSize(self, size):
- """设置固定尺寸
- :param size:
- """
- if not isinstance(size, QSize):
- size = self.SizeMedium
- self.setMinimumSize(size)
- self.setMaximumSize(size)
- self._resizePixmap()
-
- @pyqtProperty(int)
- def angle(self):
- return self._angle
-
- @angle.setter
- def angle(self, value):
- self._angle = value
- self.update()
-
- def _resizePixmap(self):
- """缩放图片
- """
- if not self._pixmap.isNull():
- self.pixmap = self._pixmap.scaled(
- self.width(), self.height(), Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
- self.update()
-
- def _resizeGifPixmap(self, _):
- """缩放动画图片
- """
- if self._movie:
- self.pixmap = self._movie.currentPixmap().scaled(
- self.width(), self.height(), Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
- self.update()
-
- def _initNetWork(self):
- """初始化异步网络库
- """
- if not hasattr(qApp, '_network'):
- network = QNetworkAccessManager(self.window())
- setattr(qApp, '_network', network)
- # 是否需要设置缓存
- if self.cacheDir and not qApp._network.cache():
- cache = QNetworkDiskCache(self.window())
- cache.setCacheDirectory(self.cacheDir)
- qApp._network.setCache(cache)
-
- def _get(self, url):
- """设置图片或者请求网络图片
- :param url:
- """
- if not url:
- self.onError('')
- return
- if url.startswith('http') and not self.loadingTimer.isActive():
- url = QUrl(url)
- request = QNetworkRequest(url)
- # request.setHeader(QNetworkRequest.UserAgentHeader, b'CAvatar')
- # request.setRawHeader(b'Author', b'Irony')
- request.setAttribute(
- QNetworkRequest.FollowRedirectsAttribute, True)
- if qApp._network.cache():
- request.setAttribute(
- QNetworkRequest.CacheLoadControlAttribute, QNetworkRequest.PreferNetwork)
- request.setAttribute(
- QNetworkRequest.CacheSaveControlAttribute, True)
- reply = qApp._network.get(request)
- self.pradius = 0
- self.loadingTimer.start(50) # 显示进度动画
- reply.finished.connect(self.onFinished)
- reply.error.connect(self.onError)
- return
- self.pradius = 0
- if os.path.exists(url) and os.path.isfile(url):
- if self.isGif:
- self._movie = QMovie(url, parent=self)
- if self._movie.isValid():
- self._movie.frameChanged.connect(self._resizeGifPixmap)
- self._movie.start()
- else:
- self._pixmap = QPixmap(url)
- self._resizePixmap()
- else:
- self.onError('')
-
-
-if __name__ == '__main__':
- import sys
- from PyQt5.QtWidgets import QApplication
-
- app = QApplication(sys.argv)
- w = CAvatar(
- img_bytes=b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x84\x00\x00\x00\x84\x08\x00\x00\x00\x00t/\xdc{\x00\x00\x00\x04gAMA\x00\x00\xb1\x8f\x0b\xfca\x05\x00\x00\x00 cHRM\x00\x00z&\x00\x00\x80\x84\x00\x00\xfa\x00\x00\x00\x80\xe8\x00\x00u0\x00\x00\xea`\x00\x00:\x98\x00\x00\x17p\x9c\xbaQ<\x00\x00\x00\x02bKGD\x00\xff\x87\x8f\xcc\xbf\x00\x00\x0e\x8fIDATx\xda\xed\x9b[\x8c]\xd7Y\xc7\xbf\xff\xb7\xd6\xde\xfb\x9c3w\xdf&\xbe\xdf\x9d\xa4\xbe\xc4\x89krmI\xdc\x06\xa5i\xa0$\x95\xa0\x95\x02U\x0bBH(\\\xfa\x80\x10<\xf0\xd6\x07\xfa\x80D\x1e\xa0P\xa2\x02-\xa1)%j\x13\xd2\xe6~O\x9a\xc4J\x9a\xc4\x8e\xeb\xcb8\xf8\x9eq\xec\x8c\xe7~\xf6\xdek}\x7f\x1e\xce\x8c\xc7g:\x05\xe7\xa0\x11\x8d\xaf\x85m\xa1o\xcf\xdb\xc7d(]\xb8\xe2\xc6\x1d\xd7\xd4\x9cYR\x0e~{/v\xdce\x89c\xa4\x93(\x8a\xb9YG\xe9\x87zox\xf9\xe4\xc3\xab\xd6\xa4!\xb1\x8b\xdb\x06H\x81\x18\xc2\x8e\xab\xde\xdd\xd7\x7f\xf4x\xfb\x95_Y0\xbf\xcd\x8fV\x10\x92\xfa\xab\xafiu\xad\xef\xff\xa8\xad\xc3k.N\x8c\xde\xe6\x02\x91X\xfb\xc8\x96\xbb\xef\xef{q%9W\xa7I8qq\xe9\xaa\xba\x7f\xe4\xaf\xd0}CI\xa9w\x94\x92\xe4|M\x92\xc1\x1f\xffp\xa0\\\xbaz\xed-\xcb\x02\xc5I\x9c\x93\x89F\xc9\xdb\xf4\xa6u#oEK"/\xae\x97\x13a\x8aP\xab{\x89\x9ai\x10I\xd2h\xa4\xd7M\xcep\xf0\xe0P\xb1\xf7\x89\x87\xfb\xcdT\xa35\xbd\xf7\x02Q\xb42Z\xe9\xbd\xb3\\e\x19\xa3\x83\\,\x88Lz\xd5\xec\xfa\xe6\xa9E\x7f\xb2\xcd\x19\xd3\xa0\xa5y\xdb\xf7\xd0\x01\xae\xfe\xf2\x1as\x05\xc5ISVq\x81\xeb(\x13\x83\x99\xa51\n`\x93\x12\xa4\xc1\x899\x03g\xf4\x1c\xc1\r?\xb87\xb5\xfdeU\x8f\xc9\xday\xae,3Ww\xce\x87\x03\x7f\xb3/n\xf9\xdaV\x9a+t:\xfd\xec\x1e\x93,\xd5\x99(c\xc6\xd2CCC\x14\xce\xd1D\x83\xe8\xcc1-\xa9\xa3~hl,\xf3y\\w\xe7\x1d\xedCY-\x94\xd5\x10\x8b\x9d\x0f\x1c\xf5\xb7\xfd\xc1<\x12\x92\x80\xd6\x1c\x04fuV\x02OD\x07\x19T\x11\x188q\x1dQ\xd5\x04\xe6\xc1\x99<\x18$\xb4]\xb5\xf2\xe4@\x84\xdf\xf6\xa5\xb5C\xbe\xeaL\xbc!>\xfb\xd0\x01\xd7yko\xae\xa9\x16\x89L\x0b\xa2\x82\xd9\xc6\xd4 \xb32d\x05\x1f\x18\xea\xd9\xbe\x99>LlCd\x1c\x1e\xec\xad\x00\xcef\xda\x86 \xbe\xef\x9f_\tze\xd7\xe9p\xd3\xe7{\x05\x9a\x87\xe7\xff\xb1\xdf\xa5\xd6\xd3\xd9\xbd\xf6\xc6++\x88\xe2%\x9ck\xa3~\xfa\x9b\x01S\x98\x90*c\x99\xaf\x8f<\xf2\x83\xb0\xf9\x97\x95\xa1\xf1\xc9A\xd3\xf2\xfb\x8f\xf7\xddq\xcf\xd5\xa9\x988kd\xb2\x84PI\x15C\x89dt\xc9\x8a\x97\x93\xb0/P\x0f\xad\xee\x8db\x95#\x8f\x7f8?J\xc6\xc1\x93\xaf\xfd\xe4\x86\xdf\\\xee\x94\xe6&\xee\x92\xa2\xc2&\x08\x98\x12A%B`\xa20=\xfe\xd8\x13\xf3v|nY\x14g"0g\xaaq\xffw\xeb\xdd/\x9e\xba\xf7\xc6\x89\x84\xeb\x9c\xfc\x9eP\xf3&\xd9\xce\x9d\x06\xcb\x13\xcf\rW\x88de\xb9\xe8W7V\xbb*\x95\x04\xa3\xfb\x1e~\xe6\xc3\xfb\xd6\xda\x94m@8M\x12\x06(\x9dF:\x13GI\xeb\xbb\xbfs\xb0\xe7\xf77v$\xb9\x06O:3\x04\x05\xaf\x1b\xda\'{~\xb2\xb9\xcdEN\xe6j\x80\x882B\x11\xad|\xf1\xfb\x07T\xb6v\xbdw|\xc1\xe7\xd6\xa9\x14\xd0\xb6\xed\xb7\xb8\xb2f\x92\xc4m\xaf\x1fy\xe7G_\xeb\xd0sL\x94\xc04\xeb(\xb2\xc2\x9bc\x84)\xf3\xddO\xbdT\xbd\xe3\x8b\xf3\x13\xe6ItA\x11|\x84\x037\xfd\xe5\xb1\xbf\xdec;\x87k\xc0\x84;\x07i>\x06u\x8cq\xfc\xcd\x9f\xf6\xf5\xa7\xba\xed\x8f\x16\xbe;\xb8hSn\xc1%%{\xf2\x98Y\t\x14X\xb4\xdf?\xbf\xf6\xf38\x1b:(Nl\x1aD\x02\xc4$OH\x8d\xbb\xdex\xb2\xbe\xfc\xcb\xd7u0\xba`e\xadT\x8d\x12\x1c(l\x93")RD\xe7\x08C#h0\x00.\x8e\x1f\xdb\xb3\xfb\xb5\x81\xaa\xc0_\xb7H\xb6\x92\xa52\x95 RG\xc5\xe2/\x0e/\x08\xbbv\xb3}\xe0\xe0x\xdb\x94q@8]\'\xdc@\xa5V*a\x07~\xfc\xf6\xc9\xee;\xefXn\xfd\x03\xebU\xd4["\xa7\xdf]\xb1\xba\xf0j \xa3\xf94u\r\xff\xa5\xc2(p&c\xc7\xf7\xee~kt\xacm\xd5\xe9T\xe5\x13\t\xcaX\x89"\xb1D\xd5\x0c6\xbe\xf3?\xde[06d\xc9G\xeboH\xa9\x93NFAk\x86\x10\x8d\xffj7\xadh\x1b\xf9\xf93?\x8f=\xb7\xff\xc6Ro\xc3\x0f\x8c\xfd\xf1\xbc$\xf8\xdc\xeb\xfb\xf7/\xf8\xf35#\x15\x8dZ\x96\x11\xddU\x11\xa3\x8a\xd0\x9c\xe3\x9b\x8f~\x84\xb1#\xa3\x95\x9eUk~e\xe9\xb7\x9e)\xbb\xf2\x07O\xec\xd8\x94\x8b\xa3\xa4\x16\x89\xfc\xf0s\x8f\xf6\xf7\x1c\xae@\xfc\xf2?\xbdR4\xb8Is\x8c\xb1\xa2\xbe\x99!\xa9<\xf7\xea\xfa\xcec\xfbG\xe7\x7f\xea\xfa\rm\x12\xb0\xf7\xf5\rB\xaay\x97\x9f\x1a\xdf\xf7\xd3{ky\xa6(\x8dZ\xf3\x93^\xdbEV\x96\xac\x7f\xeeH\xd1\xb1l\xd5\x96\r+\xab\'NE;\xfd\xf7\x1f\x9c9\xf1\xd5\xf5\xde\xa2\x9a\xa5CG_}\xf9h\xdeY\xaaI\xb2\xe0\xb7\xd6\x01\xe6\xa7,\xd4Yl\x82\xa0\xc8\x17\xd7?\xfdz\xde\xbe\xfc\xfam\xcbj\x96\xd4\xb5xe\xb8\xb7\xad\xf4\x11\xde\x92\x81\x9co~j\xa3D53a\xd5M\xe6:!\xb1\xf1\xc5wo=:x\xc5\xf2\x9ev\'\xe1\xe8\xb1\xb2"\xfb\x1c_\xdd\xb4\xc2\xb2$\x1f;\xbd\xf7\xcd\x83\x83\x03d\x99*C\xf5\x9e\x9b\xb3hv\xd6\xdbR46[Gte\xe7\xcd\xdb\x8f\xd4\x17t\xa5!#\xea\xa9\xbe\xf1\xfa\xf85\xa9DoQR\xa5\x9d8q\x95\x830\x880=\x1b:\xd2zE\xcbl\xf3U\x96\xb0t$\xf6\x9d\xaa\x85\xf2K\xd9\x8bG\x9f\xba9\x7f\xf6\xf8\xf0\xc0\xe9\xd14\xe9\xea9\xed0\xae\xa2\x8b\xcbg\xb6\xcdO\xf4\xac\xc7WFi\x86pTZ\xba\xca\x993\x15\x81\x8f\xe5sG\xe6_i\nz\xcb\x13\x85\x1b?Z8\x15\x94\x14\xd5\xb3\xc9VH\x83x+UIOp\xf4\xa0\x85$Y\x97\xbe\x15O\xed\xb9\xde\x8e\x9c\t\xc9\xc6\r\x1b\x92G\xcf\x98T"]\xdf!\xfd\xb3OM\xa5\xeeT-\x1d\xa79+1\xd0\x91E\xca\xe8$w\xfb_\xaal\xe9\xf4\x88\xce\xe8\x83P\xfdXLJg\x12\'*\xfe\t\xe5\x92F\xb6AB\xa8g\x8e\x88+\xf0\x0fE\x9e\r\xbf\xf3\x85\xaf|\xf6\x0c\xb1\xaa-\xff\xbb}\xc1G\x13\xb8z\xfb\x86\x95\x0eg}\x15\x08\xfa\xb2\xd9D)\x80D\xaf\xb4\xac \xb2\x0f\xbf;\xda~}\x879\xb1\xa4\x9eh`6:f\xd1G\x1d\x8f\x8ej\xaa\xb0\x08B\x04l\x04v\x08\xcc\x1f\xeaW\xbba\xf1cQ\x87\xab\x03\x83m\xeb\xbc9;\xfa\xed\xc7\xb3\xb4\x84\xa8\x8dm\xfc\xf5\xdbj\xa5\x9emR\x10\x94\xc25C4B\x98\xc9\xe8\x07m\xdd\x15\x94/\xec\xd1y\xeb4\x1a\x90\'\xc6U=\x1fU\xbb3\x8a\xa0\x1e`TD\xc5\xf4n\x1a\x95\x87sm\xbf\xed\x97^\x18f\xc5\xf5\x8d\xb5k\x94\x88\xa3\xbbk\x89Q\x1ct\xdeo\x7f2\r\x06L\xe6\x86\xa0x\x86\xe9\xd7A\x80\xe2\xfb\x1e\x1a\xbcv\xf5R{\xb6\xbf\xb2\xf9\n\x1a\x9dDO\xbb\xfa\xb3Ovm\xf0\x14\xf3\xa5\x08\x89F\xf0l\x86\x80\xf0x\xb0x\xfa\xbdz\x18\xaf\xb8b\xb8\xc7\xa7\xac\xcb\x8a\xd5\xa7G\xa5=\x8f\xea\xee\xd8R\xb3\xe8t*\xab1&h\x0e`P\t\xa2\xd4|+\x9f\xfc\x9et\x97\xb1\xd6\xb3\xbd\xdd\x92\x02\x96F\xb0\xfb\xabwe\x0b\x05T\xb3\x02"4\x17\xb5\x11\xc6\xcf5\xf2\xe2X\xe9\x86\xff%=\xb3\xa4\xf3\xc4\x90\r\xfbXH\xac\xcd\xbf\xefo\x0f\xb7\xa7\xfd\xfda\xf1u]#i\xd4\xa9\x9e\n\x9c\x19\x9b!h\x02E)p\xd7,]\xfd\xfc\x81\xc1\xb6|\xde\x92\xb4\xee\xd0hD\x96\xd9R\x88\x98\xa8\xa5\xa6g\x1bR\xcd\x08\x8a\xb1\x0f\xa1\xba\xa0\xb6\xe4\xa6\xe5\xdf\xa8&\x87\xb7\xa83\x89\x95\x15_?\xe1:\xef\x1f@\xe7\x92\x90\xd1\x89\x9e\xcdX\xa3\xea\xf4\xa4F#\x1d\x98E\x1d\x98\xdfqO\xf1^w\xbd}\xe0\x85\xb6\xe5\x05(\xd1\x19\xd4\x94B\x88Z{57j\x9c^\x99QDlt$)z>s\xab.x\xdf\xb4\xb4\xe8\x02\x13\x11\xce\x9f\xe7\x07+ERs \xc4\xa6r\x1aG\x89:\xbd\x02\xf3e\xb0\xc2b\xcfx\xb5\xef\r7\x86\xd1\xfe\x07\xbf\xf1\xbe\x0b\n\x13\x15\xa2\x91>\x98u\xb8\xe8\xb3\xe8\xcb\xc4\xceM4)B\xd1\xfa`\x90+n\\\xb5\xb8+\xb2\x88=\tM\x0c\x02c=D/\xb5\x84"T\xea\xd4\x1e\xea\xb42\xd0`\x89w\x11(\xed\xd8\xbf\xedcm\xdb\'+\xd8\xf5\xc8GY\x107\x95Z#$\x1d\xed\xc2\xae\x00\x15\x9e\xdbjhTi\x83\x92b\xc9Rbx Ij\x8b\x11+\tb\x8c\xaa\xcey\'\xa9\nM\xcek>5{\xcc\x18\x92Rq\xc6\xe7\x87\x1e\xf9\xd9p\xc7=_\x18\xfb\xa7\x97\xf4\x95\x9b\x17E\x7fN\xa9C\xfaR\xa4gm\x12M8\xbd\xa0\xd3\x98\xa4!\xac\xaf\xf9\x0f\x1ey\xb3\xae+\x17\x05\xa3@E\xa20\x86R3\x9d\xb1\xc5\xd0\x04\x11\x90&c\x08?xmtt\xa0\xed\xda_\xdb\xa1\xbd__\xfb\x9f~$:\xb3svk\xfc`\x90\xb5e\x1a\xd0\xe8\x07\x9c#\n\x9a.\xdf\xfaN\xba\xd9\x15\xed\xe9\t\xad\xdd\xd8\x053\nI\x07U\xe7\xac\xa63\xb6g\xa7\xc5\x8e\xa8\x80\xdbpr_\xbef\xfbg\xd6%\xa5u\xde\xb9|dC\xb3\xde\xc0\x99H\x1c\xab\xd7\xbd64\xe5\xdc\x9fA\xba\x7f\xe7)Y\xa9\xec\xba\xbdvp\xc5\xed,\x12\x90\x02%\x85P\xa9\x81\xa4^\x18\x820&!\xbde\xfb{\x83\xcbV\'&i\xa9\xdd\xb7\x8e\xd7\xc6*\xe7\x1a"`\xf3\xdaF\\ThP\xb5f\x131\xc8\x86\xd5E\xad^\xb1ewGICb\x10\x80\x86\x86<2\x87\x8bJB\x90D\xd1\x02\xb5\x1b-\x82N"\x83\xe6\xde &Sb\xa4\xc8\xc2\x8a3\xe7\x83sFLS\n\xd1\\\xdaC\x1aM\xbd\x17q\x9cL\xc6)\x0e!\xebp\x86\x19*\xd8&\xd9 \xc2\xb1\xac$,\xa2(,\x8a\x8395\x1f\xa1r\xb6cD\x8d\xce\x99\xf7\x9c\xa9\xbf\xa2\xa4\xf3\xa2l\xb4\xa2qN^\xaf\x14\xf8\nDf\xa8\xdb\x9aN"\x023\x1f\xc4\xab\xc5(NH\x07\xa7Z\x89q\xaax\x84D\xc4H\xf5\xb34\x91\xe8@B\xed\xbc\x89\x82\x95\xb1\xda6s\xff\xad\tB#,W\xd2\xe8\xb3FAS7Z\x18\xcf\xdc\x94\xd4\xa9\x96\x8e\x97\xd0Tg\xaa\xca\'Z(!:\rM2\x16\x0b\xb9\xfav\xc1E\'?11QS\x80f*F \x05\xa9\x12\x85S\x9b\xe9C=h-\x99\xf13\tH\x8a\xa9\xd0\xa6\xc9\x98\xc5\x88\xf75@f\x18\xba4\xeb\x84\x89\x98\x906YcR\x8ch\x8c\xd1l\nbl\xbcd\xcd\xdb\xcc=7*\xe8Hh\xd3\xd0\x8d\xe0\xf8\x10;\xdb`3\xb5\xa7\xcf;\xa9Y^\x13i\\C\xbf\xd98"\xe8\xa1\x11\xeb\xae\xaa5\xa6~\xcd\x97B\xd8\xe4\xeco\xf2\x84\xc6\x800=U/{:\xa8q.\x10\x17X\xa0\x13\x03J\xd9\x1b\xa4\xbb\x1a\xe6\xb6\x15\x12@\xb8\xc8\x8f\xa2-H8\xd3\xf4\xe9\xd2\x86\xb3&\xa6\x81\xc9\x81\x83.[\x98\xccq.Eq"f\x8eG\x86\xb18\x99\xb9\x1dz\t\x10\x84\xa8+S\x8c<\xdeWV\xd7\xcc\xa5\xdf\xdd\xd8\xe6\n\xa81\xef\xa3\xaeP\\\xea\\\xf4\xbc\x15]\x8c\x9e\x1f>\xfd\xac\xc3\xda\x95\xca\x8b\xb5\xfd\x1b\x0b\x16T\x04\xf8\xb0/\xce[,\x98\xd1\xbb\\\x02\x044z\x1c\x7fl\xd7\xf1q\xb8\x1bz\\\xa0\xcdI\x8c\xae\xac\x14\x86\xf8\xce \x16\xcf\x9b6\xc0\xbe,Id\xa3i\xa5\xd6\xd1\xdb\xbb\xf6\x13;\x1cA77\xad\xa8\x8cV\xeb\xd9\xae\x87$\xb9\xba\xcbh\xfa?\xbc\x0e\x1d\xcf\xd8s\xef@\xe1\xabH\x181G\xcd4&\xca\xa7~tX\x16m\xb3Y&i\x97\x00AT\xc7\x1dc\x97\xc1\x11\x91P\x9b\xd36U\x8e\xa7?;\xbc\xa0\xbeu\xa3\xcd\xa2\xca\x98\xfb\xbf\xdb\xc0\x8a\xaa\x04U"\x84D\x04s\x1b\x1e\x13E\xd5dh$\xef\xaeVl\x96=\x97\x00A\x8d\xde\x08\x81\x01\x86\x99\x1d\xf0\x0c\x82\x08\xe2\x83\x13\xf3\xf5t6A\\\x8a\x9f@\xf4\x91\r\xf7M\x05q\xf1I\xa9\x88H\xd4$x#\xa2\x9f}\xb4z\t\x92\xf8\xdf[\xff\xe7\xfe\xa6\xa6\x05\xd1\x82hA\xb4 Z\x10\x1f\xfbjA\xb4 Z\x10-\x88\x16D\x0b\xa2\x05\xd1\x82\xf8\x7f\x01\xf1\xdf\x91\xd3\xe9`7\x9a\x1c\x88\x00\x00\x00\x12tEXtexif:ExifOffset\x00620\x1a\xa3x\x00\x00\x00\x12tEXtexif:ImageLength\x000\xc1\xc5N\xce\x00\x00\x00\x11tEXtexif:ImageWidth\x000/\xffv\xa0\x00\x00\x00\x12tEXtexif:LightSource\x000x\x05kH\x00\x00\x00\x00IEND\xaeB`\x82'
- ,
- url='https://wx.qlogo.cn/mmhead/ver_1/DpDqmvTDORNWfLrMj26YicorEUREffl1G8FapawdKgINVH9g1icudfWesGrH9LqeGAz16z4PmkW9U1KAIM3btWgozZ1GaLF66bdKdxlMdazmibn2hpFeiaa4613dN6HM4Vfk/132')
- w.show()
- sys.exit(app.exec_())
diff --git a/app/components/QCursorGif.py b/app/components/QCursorGif.py
deleted file mode 100644
index 01d4800..0000000
--- a/app/components/QCursorGif.py
+++ /dev/null
@@ -1,71 +0,0 @@
-#!/usr/bin/env python
-# -*- coding: utf-8 -*-
-
-"""
-Created on 2020年3月13日
-@author: Irony
-@site: https://pyqt.site , https://github.com/PyQt5
-@email: 892768447@qq.com
-@file: Demo.Lib.QCursorGif
-@description:
-"""
-
-try:
- from PyQt5.QtCore import QTimer, Qt
- from PyQt5.QtGui import QCursor, QPixmap
- from PyQt5.QtWidgets import QApplication
-except ImportError:
- from PySide2.QtCore import QTimer, Qt
- from PySide2.QtGui import QCursor, QPixmap
- from PySide2.QtWidgets import QApplication
-from app.resources import resource_rc
-
-var = resource_rc.qt_resource_name
-
-
-class QCursorGif:
-
- def initCursor(self, cursors, parent=None):
- # 记录默认的光标
- self._oldCursor = Qt.ArrowCursor
- self.setOldCursor(parent)
- # 加载光标图片
- self._cursorImages = [
- QCursor(QPixmap(cursor)) for cursor in cursors]
- self._cursorIndex = 0
- self._cursorCount = len(self._cursorImages) - 1
- # 创建刷新定时器
- self._cursorTimeout = 200
- self._cursorTimer = QTimer(parent)
- self._cursorTimer.timeout.connect(self._doBusy)
- self.num = 0
-
- def _doBusy(self):
- if self._cursorIndex > self._cursorCount:
- self._cursorIndex = 0
- QApplication.setOverrideCursor(
- self._cursorImages[self._cursorIndex])
- self._cursorIndex += 1
- self.num += 1
-
- def startBusy(self):
- # QApplication.setOverrideCursor(Qt.WaitCursor)
- if not self._cursorTimer.isActive():
- self._cursorTimer.start(self._cursorTimeout)
-
- def stopBusy(self):
- self._cursorTimer.stop()
- QApplication.restoreOverrideCursor()
- # 将光标出栈,恢复至原始状态
- for i in range(self.num):
- QApplication.restoreOverrideCursor()
- self.num = 0
-
-
- def setCursorTimeout(self, timeout):
- self._cursorTimeout = timeout
-
- def setOldCursor(self, parent=None):
- QApplication.overrideCursor()
- self._oldCursor = (QApplication.overrideCursor() or parent.cursor() or Qt.ArrowCursor or Qt.IBeamCursor) if parent else (
- QApplication.overrideCursor() or Qt.ArrowCursor)
diff --git a/app/components/__init__.py b/app/components/__init__.py
deleted file mode 100644
index 6d1eb7f..0000000
--- a/app/components/__init__.py
+++ /dev/null
@@ -1,2 +0,0 @@
-from .contact_info_ui import ContactQListWidgetItem
-from .scroll_bar import ScrollBar
diff --git a/app/components/bubble_message.py b/app/components/bubble_message.py
deleted file mode 100644
index d2b5e0c..0000000
--- a/app/components/bubble_message.py
+++ /dev/null
@@ -1,301 +0,0 @@
-import os.path
-import subprocess
-import platform
-
-from PyQt5 import QtGui
-from PyQt5.QtCore import QSize, pyqtSignal, Qt, QThread
-from PyQt5.QtGui import QPainter, QFont, QColor, QPixmap, QPolygon, QFontMetrics
-from PyQt5.QtWidgets import QWidget, QLabel, QHBoxLayout, QSizePolicy, QVBoxLayout, QSpacerItem, \
- QScrollArea
-
-from app.components.scroll_bar import ScrollBar
-
-
-class MessageType:
- Text = 1
- Image = 3
-
-
-class TextMessage(QLabel):
- heightSingal = pyqtSignal(int)
-
- def __init__(self, text, is_send=False, parent=None):
- if isinstance(text, bytes):
- text = text.decode('utf-8')
- super(TextMessage, self).__init__(text, parent)
- font = QFont('微软雅黑', 12)
- self.setFont(font)
- self.setWordWrap(True)
- self.setMaximumWidth(800)
- # self.setMinimumWidth(100)
- self.setMinimumHeight(45)
- self.setTextInteractionFlags(Qt.TextSelectableByMouse)
- self.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
- if is_send:
- self.setAlignment(Qt.AlignCenter | Qt.AlignRight)
- self.setStyleSheet(
- '''
- background-color:#b2e281;
- border-radius:10px;
- padding:10px;
- '''
- )
- else:
- self.setStyleSheet(
- '''
- background-color:white;
- border-radius:10px;
- padding:10px;
- '''
- )
- font_metrics = QFontMetrics(font)
- rect = font_metrics.boundingRect(text)
- # rect = font_metrics
- self.setMaximumWidth(rect.width() + 40)
-
- def paintEvent(self, a0: QtGui.QPaintEvent) -> None:
- super(TextMessage, self).paintEvent(a0)
-
-
-class Triangle(QLabel):
- def __init__(self, Type, is_send=False, position=(0, 0), parent=None):
- """
-
- @param Type:
- @param is_send:
- @param position:(x,y)
- @param parent:
- """
- super().__init__(parent)
- self.Type = Type
- self.is_send = is_send
- self.position = position
-
- def paintEvent(self, a0: QtGui.QPaintEvent) -> None:
-
- super(Triangle, self).paintEvent(a0)
- if self.Type == MessageType.Text:
- self.setFixedSize(6, 45)
- painter = QPainter(self)
- triangle = QPolygon()
- x, y = self.position
- if self.is_send:
- painter.setPen(QColor('#b2e281'))
- painter.setBrush(QColor('#b2e281'))
- triangle.setPoints(0, 20+y, 0, 34+y, 6, 27+y)
- else:
- painter.setPen(QColor('white'))
- painter.setBrush(QColor('white'))
- triangle.setPoints(0, 27+y, 6, 20+y, 6, 34+y)
- painter.drawPolygon(triangle)
-
-
-class Notice(QLabel):
- def __init__(self, text, type_=3, parent=None):
- super().__init__(text, parent)
- self.type_ = type_
- self.setFont(QFont('微软雅黑', 10))
- self.setWordWrap(True)
- self.setTextInteractionFlags(Qt.TextSelectableByMouse)
- self.setAlignment(Qt.AlignCenter)
-
-
-class Avatar(QLabel):
- def __init__(self, avatar, parent=None):
- super().__init__(parent)
- if isinstance(avatar, str):
- self.setPixmap(QPixmap(avatar).scaled(45, 45))
- self.image_path = avatar
- elif isinstance(avatar, QPixmap):
- self.setPixmap(avatar.scaled(45, 45))
- self.setFixedSize(QSize(45, 45))
-
-
-def open_image_viewer(file_path):
- system_platform = platform.system()
-
- if system_platform == "Darwin": # macOS
- subprocess.run(["open", file_path])
- elif system_platform == "Windows":
- subprocess.run(["start", " ", file_path], shell=True)
- elif system_platform == "Linux":
- subprocess.run(["xdg-open", file_path])
- else:
- print("Unsupported platform")
-
-
-class OpenImageThread(QThread):
- def __init__(self, image_path):
- super().__init__()
- self.image_path = image_path
-
- def run(self) -> None:
- if os.path.exists(self.image_path):
- open_image_viewer(self.image_path)
-
-
-class ImageMessage(QLabel):
- def __init__(self, image, is_send, image_link='', max_width=480, max_height=240, parent=None):
- """
- param:image 图像路径或者QPixmap对象
- param:image_link='' 点击图像打开的文件路径
- """
- super().__init__(parent)
- self.image = QLabel(self)
- self.max_width = max_width
- self.max_height = max_height
- # self.setFixedSize(self.max_width,self.max_height)
- self.setMaximumWidth(self.max_width)
- self.setMaximumHeight(self.max_height)
- self.setCursor(Qt.PointingHandCursor)
- if isinstance(image, str):
- pixmap = QPixmap(image)
- self.image_path = image
- elif isinstance(image, QPixmap):
- pixmap = image
- self.set_image(pixmap)
- if image_link:
- self.image_path = image_link
- self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
-
- if is_send:
- self.setAlignment(Qt.AlignCenter | Qt.AlignRight)
- # self.setScaledContents(True)
-
- def set_image(self, pixmap):
- # 计算调整后的大小
- adjusted_width = min(pixmap.width(), self.max_width)
- adjusted_height = min(pixmap.height(), self.max_height)
- self.setPixmap(pixmap.scaled(adjusted_width, adjusted_height, Qt.KeepAspectRatio))
- # 调整QLabel的大小以适应图片的宽高,但不超过最大宽高
- # self.setFixedSize(adjusted_width, adjusted_height)
-
- def mousePressEvent(self, event):
- if event.buttons() == Qt.LeftButton: # 左键按下
- print('打开图像', self.image_path)
- self.open_image_thread = OpenImageThread(self.image_path)
- self.open_image_thread.start()
-
-
-class BubbleMessage(QWidget):
- def __init__(self, str_content, avatar, Type, is_send=False, display_name=None, parent=None):
- super().__init__(parent)
- self.isSend = is_send
- # self.set
- self.setStyleSheet(
- '''
- border:none;
- '''
- )
- layout = QHBoxLayout()
- layout.setSpacing(0)
- layout.setContentsMargins(0, 5, 5, 5)
- # self.resize(QSize(200, 50))
- self.avatar = Avatar(avatar)
- triangle = Triangle(Type, is_send, (0, 0))
- if Type == MessageType.Text:
- self.message = TextMessage(str_content, is_send)
- # self.message.setMaximumWidth(int(self.width() * 0.6))
- elif Type == MessageType.Image:
- self.message = ImageMessage(str_content, is_send)
- else:
- raise ValueError("未知的消息类型")
- if display_name:
- triangle = Triangle(Type, is_send, (0, 10))
- label_name = QLabel(display_name, self)
- label_name.setFont(QFont('微软雅黑', 10))
- if is_send:
- label_name.setAlignment(Qt.AlignRight)
- vlayout = QVBoxLayout()
- vlayout.setSpacing(0)
- if is_send:
- vlayout.addWidget(label_name, 0, Qt.AlignTop | Qt.AlignRight)
- vlayout.addWidget(self.message, 0, Qt.AlignTop | Qt.AlignRight)
- else:
- vlayout.addWidget(label_name)
- vlayout.addWidget(self.message)
- self.spacerItem = QSpacerItem(45 + 6, 45, QSizePolicy.Expanding, QSizePolicy.Minimum)
- if is_send:
- layout.addItem(self.spacerItem)
- if display_name:
- layout.addLayout(vlayout, 1)
- else:
- layout.addWidget(self.message, 1)
- layout.addWidget(triangle, 0, Qt.AlignTop | Qt.AlignLeft)
- layout.addWidget(self.avatar, 0, Qt.AlignTop | Qt.AlignLeft)
- else:
- layout.addWidget(self.avatar, 0, Qt.AlignTop | Qt.AlignRight)
- layout.addWidget(triangle, 0, Qt.AlignTop | Qt.AlignRight)
- if display_name:
- layout.addLayout(vlayout, 1)
- else:
- layout.addWidget(self.message, 1)
- layout.addItem(self.spacerItem)
- self.setLayout(layout)
-
-
-class ScrollAreaContent(QWidget):
- def __init__(self, parent=None):
- super().__init__(parent)
- self.adjustSize()
-
-
-class ScrollArea(QScrollArea):
- def __init__(self, parent=None):
- super().__init__(parent)
- self.setWidgetResizable(True)
- self.setHorizontalScrollBarPolicy(Qt.ScrollBarAlwaysOff)
- self.setStyleSheet(
- '''
- border:none;
- '''
- )
-
-
-class ChatWidget(QWidget):
- def __init__(self):
- super().__init__()
- self.resize(500, 200)
-
- layout = QVBoxLayout()
- layout.setSpacing(0)
- self.adjustSize()
- # 生成滚动区域
- self.scrollArea = ScrollArea(self)
- scrollBar = ScrollBar()
- self.scrollArea.setVerticalScrollBar(scrollBar)
- # 生成滚动区域的内容部署层部件
- self.scrollAreaWidgetContents = ScrollAreaContent(self.scrollArea)
- self.scrollAreaWidgetContents.setMinimumSize(50, 100)
- # 设置滚动区域的内容部署部件为前面生成的内容部署层部件
- self.scrollArea.setWidget(self.scrollAreaWidgetContents)
- layout.addWidget(self.scrollArea)
- self.layout0 = QVBoxLayout()
- self.layout0.setSpacing(0)
- self.scrollAreaWidgetContents.setLayout(self.layout0)
- self.setLayout(layout)
-
- def add_message_item(self, bubble_message, index=1):
- if index:
- self.layout0.addWidget(bubble_message)
- else:
- self.layout0.insertWidget(0, bubble_message)
- # self.set_scroll_bar_last()
-
- def set_scroll_bar_last(self):
- self.scrollArea.verticalScrollBar().setValue(
- self.scrollArea.verticalScrollBar().maximum()
- )
-
- def set_scroll_bar_value(self, val):
- self.verticalScrollBar().setValue(val)
-
- def verticalScrollBar(self):
- return self.scrollArea.verticalScrollBar()
-
- def update(self) -> None:
- super().update()
- self.scrollAreaWidgetContents.adjustSize()
- self.scrollArea.update()
- # self.scrollArea.repaint()
- # self.verticalScrollBar().setMaximum(self.scrollAreaWidgetContents.height())
diff --git a/app/components/calendar_dialog.py b/app/components/calendar_dialog.py
deleted file mode 100644
index 91898ef..0000000
--- a/app/components/calendar_dialog.py
+++ /dev/null
@@ -1,78 +0,0 @@
-import time
-
-from datetime import datetime, timedelta
-from PyQt5.QtCore import QTimer, QThread, pyqtSignal, Qt
-from PyQt5.QtGui import QIcon
-from PyQt5.QtWidgets import QApplication, QDialog, QCheckBox, QMessageBox, QCalendarWidget, QWidget, QVBoxLayout, \
- QToolButton
-
-from app.ui.Icon import Icon
-
-
-class CalendarDialog(QDialog):
- selected_date_signal = pyqtSignal(int)
-
- def __init__(self, date_range=None, parent=None):
- """
-
- @param date_range: tuple[Union[QDate, datetime.date],Union[QDate, datetime.date]] #限定的可选择范围
- @param parent:
- """
- super().__init__(parent)
- self.setWindowTitle('选择日期')
- self.calendar = QCalendarWidget(self)
- self.calendar.clicked.connect(self.onDateChanged)
- prev_btn = self.calendar.findChild(QToolButton, "qt_calendar_prevmonth")
- prev_btn.setIcon(Icon.Arrow_left_Icon)
- next_btn = self.calendar.findChild(QToolButton, "qt_calendar_nextmonth")
- next_btn.setIcon(Icon.Arrow_right_Icon)
- self.date_range = date_range
- if date_range:
- self.calendar.setDateRange(*date_range)
- # 从第一天开始,依次添加日期到列表,直到该月的最后一天
- current_date = date_range[1]
- while (current_date + timedelta(days=1)).month == date_range[1].month:
- current_date += timedelta(days=1)
- range_format = self.calendar.dateTextFormat(current_date)
- range_format.setForeground(Qt.gray)
- self.calendar.setDateTextFormat(current_date, range_format)
- # 从第一天开始,依次添加日期到列表,直到该月的最后一天
- current_date = date_range[0]
- while (current_date - timedelta(days=1)).month == date_range[0].month:
- current_date -= timedelta(days=1)
- range_format = self.calendar.dateTextFormat(current_date)
- range_format.setForeground(Qt.gray)
- self.calendar.setDateTextFormat(current_date, range_format)
- layout = QVBoxLayout(self)
- layout.addWidget(self.calendar)
- self.setLayout(layout)
-
- def set_start_date(self):
- if self.date_range:
- self.calendar.setCurrentPage(self.date_range[0].year, self.date_range[0].month)
- def set_end_date(self):
- if self.date_range:
- self.calendar.setCurrentPage(self.date_range[1].year, self.date_range[1].month)
- def onDateChanged(self):
- # 获取选择的日期
- selected_date = self.calendar.selectedDate()
- s_t = time.strptime(selected_date.toString("yyyy-MM-dd"), "%Y-%m-%d") # 返回元祖
- mkt = int(time.mktime(s_t))
- timestamp = mkt
- self.selected_date_signal.emit(timestamp)
- print("Selected Date:", selected_date.toString("yyyy-MM-dd"), timestamp)
- self.close()
-
-
-if __name__ == '__main__':
- import sys
- from datetime import datetime, timedelta
-
- app = QApplication(sys.argv)
- # 设置日期范围
- start_date = datetime(2024, 1, 5)
- end_date = datetime(2024, 1, 9)
- date_range = (start_date.date(), end_date.date())
- ex = CalendarDialog(date_range=date_range)
- ex.show()
- sys.exit(app.exec_())
diff --git a/app/components/contact_info_ui.py b/app/components/contact_info_ui.py
deleted file mode 100644
index ba73ea7..0000000
--- a/app/components/contact_info_ui.py
+++ /dev/null
@@ -1,104 +0,0 @@
-import sys
-
-from PyQt5.Qt import *
-from PyQt5.QtCore import *
-from PyQt5.QtWidgets import *
-
-from .CAvatar import CAvatar
-
-Stylesheet = """
-QWidget{
- background: rgb(238,244,249);
-}
-"""
-Stylesheet_hover = """
-QWidget,QLabel{
- background: rgb(230, 235, 240);
-}
-"""
-Stylesheet_clicked = """
-QWidget,QLabel{
- background: rgb(230, 235, 240);
-}
-"""
-
-
-class QListWidgetItemWidget(QWidget):
- def __init__(self):
- super().__init__()
- self.is_selected = False
-
- def leaveEvent(self, e): # 鼠标离开label
- if self.is_selected:
- return
- self.setStyleSheet(Stylesheet)
-
- def enterEvent(self, e): # 鼠标移入label
- self.setStyleSheet(Stylesheet_hover)
-
-
-# 自定义的item 继承自QListWidgetItem
-class ContactQListWidgetItem(QListWidgetItem):
- def __init__(self, name, url, img_bytes=None):
- super().__init__()
- # 自定义item中的widget 用来显示自定义的内容
- self.widget = QListWidgetItemWidget()
- # 用来显示name
- self.nameLabel = QLabel(self.widget)
- self.nameLabel.setText(name)
- # 用来显示avator(图像)
- self.avatorLabel = CAvatar(parent=self.widget, shape=CAvatar.Rectangle, size=QSize(60, 60),
- url=url, img_bytes=img_bytes)
- # 设置布局用来对nameLabel和avatorLabel进行布局
- hbox = QHBoxLayout()
- hbox.addWidget(self.avatorLabel)
- hbox.addWidget(self.nameLabel)
- hbox.addStretch(1)
- # 设置widget的布局
- self.widget.setLayout(hbox)
- self.widget.setStyleSheet(Stylesheet)
- # 设置自定义的QListWidgetItem的sizeHint,不然无法显示
- self.setSizeHint(self.widget.sizeHint())
-
- def select(self):
- """
- 设置选择后的事件
- @return:
- """
- self.widget.is_selected = True
- self.widget.setStyleSheet(Stylesheet_clicked)
-
- def dis_select(self):
- """
- 设置取消选择的事件
- @return:
- """
- self.widget.is_selected = False
- self.widget.setStyleSheet(Stylesheet)
-
-
-if __name__ == "__main__":
- app = QApplication(sys.argv)
-
- # 主窗口
- w = QWidget()
- w.setWindowTitle("QListWindow")
- # 新建QListWidget
- listWidget = QListWidget(w)
- listWidget.resize(300, 300)
-
- # 新建两个自定义的QListWidgetItem(customQListWidgetItem)
- item1 = ContactQListWidgetItem("鲤鱼王", "liyuwang.jpg")
- item2 = ContactQListWidgetItem("可达鸭", "kedaya.jpg")
-
- # 在listWidget中加入两个自定义的item
- listWidget.addItem(item1)
- listWidget.setItemWidget(item1, item1.widget)
- listWidget.addItem(item2)
- listWidget.setItemWidget(item2, item2.widget)
-
- # 绑定点击槽函数 点击显示对应item中的name
- listWidget.itemClicked.connect(lambda item: print(item.nameLabel.text()))
-
- w.show()
- sys.exit(app.exec_())
diff --git a/app/components/export_contact_item.py b/app/components/export_contact_item.py
deleted file mode 100644
index 9566bf5..0000000
--- a/app/components/export_contact_item.py
+++ /dev/null
@@ -1,127 +0,0 @@
-import sys
-
-from PyQt5.Qt import *
-from PyQt5.QtCore import *
-from PyQt5.QtWidgets import *
-
-try:
- from .CAvatar import CAvatar
-except:
- from CAvatar import CAvatar
-
-Stylesheet = """
-QWidget{
- background: rgb(238,244,249);
-}
-"""
-Stylesheet_hover = """
-QWidget,QLabel{
- background: rgb(230, 235, 240);
-}
-"""
-Stylesheet_clicked = """
-QWidget,QLabel{
- background: rgb(230, 235, 240);
-}
-"""
-
-
-class QListWidgetItemWidget(QWidget):
- def __init__(self):
- super().__init__()
- self.is_selected = False
-
- def leaveEvent(self, e): # 鼠标离开label
- if self.is_selected:
- return
- self.setStyleSheet(Stylesheet)
-
- def enterEvent(self, e): # 鼠标移入label
- self.setStyleSheet(Stylesheet_hover)
-
-
-# 自定义的item 继承自QListWidgetItem
-class ContactQListWidgetItem(QListWidgetItem):
- def __init__(self, name, url, img_bytes=None):
- super().__init__()
- self.is_select = False
- # 自定义item中的widget 用来显示自定义的内容
- self.widget = QListWidgetItemWidget()
- # 用来显示name
- self.nameLabel = QLabel(self.widget)
- self.nameLabel.setText(name)
- # 用来显示avator(图像)
- self.avatorLabel = CAvatar(parent=self.widget, shape=CAvatar.Rectangle, size=QSize(30, 30),
- url=url, img_bytes=img_bytes)
- # 设置布局用来对nameLabel和avatorLabel进行布局
- hbox = QHBoxLayout(self.widget)
- self.checkBox = QCheckBox(self.widget)
- self.checkBox.clicked.connect(self.select)
- hbox.addWidget(self.checkBox)
- hbox.addWidget(self.avatorLabel)
- hbox.addWidget(self.nameLabel)
- hbox.addStretch(1)
- # 设置widget的布局
- self.widget.setLayout(hbox)
- self.widget.setStyleSheet(Stylesheet)
- # 设置自定义的QListWidgetItem的sizeHint,不然无法显示
- self.setSizeHint(self.widget.sizeHint())
- sizePolicy = QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
- sizePolicy.setHorizontalStretch(0)
- sizePolicy.setVerticalStretch(0)
- sizePolicy.setHeightForWidth(self.widget.sizePolicy().hasHeightForWidth())
- self.widget.setSizePolicy(sizePolicy)
-
- def select(self):
- """
- 设置选择后的事件
- @return:
- """
- self.widget.is_selected = True
- self.is_select = not self.is_select
- # print('选择',self.is_select)
- self.checkBox.setChecked(self.is_select)
- # self.widget.setStyleSheet(Stylesheet_clicked)
-
- def force_select(self):
- self.is_select = True
- self.checkBox.setChecked(self.is_select)
-
- def force_dis_select(self):
- self.is_select = False
- self.checkBox.setChecked(self.is_select)
-
- def dis_select(self):
- """
- 设置取消选择的事件
- @return:
- """
- self.widget.is_selected = False
- self.widget.setStyleSheet(Stylesheet)
-
-
-if __name__ == "__main__":
- app = QApplication(sys.argv)
-
- # 主窗口
- w = QWidget()
- w.setWindowTitle("QListWindow")
- # 新建QListWidget
- listWidget = QListWidget(w)
- listWidget.resize(300, 300)
-
- # 新建两个自定义的QListWidgetItem(customQListWidgetItem)
- item1 = ContactQListWidgetItem("鲤鱼王", "liyuwang.jpg")
- item2 = ContactQListWidgetItem("可达鸭", "kedaya.jpg")
-
- # 在listWidget中加入两个自定义的item
- listWidget.addItem(item1)
- listWidget.setItemWidget(item1, item1.widget)
- listWidget.addItem(item2)
- listWidget.setItemWidget(item2, item2.widget)
-
- # 绑定点击槽函数 点击显示对应item中的name
- listWidget.itemClicked.connect(lambda item: item.select())
-
- w.show()
- sys.exit(app.exec_())
diff --git a/app/components/prompt_bar.py b/app/components/prompt_bar.py
deleted file mode 100644
index df99b1f..0000000
--- a/app/components/prompt_bar.py
+++ /dev/null
@@ -1,39 +0,0 @@
-from PyQt5 import QtGui
-from PyQt5.QtCore import *
-from PyQt5.QtGui import *
-from PyQt5.QtWidgets import *
-
-
-class PromptBar(QLabel):
- def __init__(self, parent=None):
- super().__init__(parent)
-
- def paintEvent(self, e): # 绘图事件
- qp = QPainter()
- qp.begin(self)
- self.drawRectangles1(qp) # 绘制线条矩形
- self.drawRectangles2(qp) # 绘制填充矩形
- self.drawRectangles3(qp) # 绘制线条+填充矩形
- self.drawRectangles4(qp) # 绘制线条矩形2
- qp.end()
-
- def drawRectangles1(self, qp): # 绘制填充矩形
- qp.setPen(QPen(Qt.black, 2, Qt.SolidLine)) # 颜色、线宽、线性
- qp.drawRect(*self.data)
-
- def drawRectangles2(self, qp): # 绘制填充矩形
- qp.setPen(QPen(Qt.black, 2, Qt.NoPen))
- qp.setBrush(QColor(200, 0, 0))
- qp.drawRect(220, 15, 200, 100)
-
- def drawRectangles3(self, qp): # 绘制线条+填充矩形
- qp.setPen(QPen(Qt.black, 2, Qt.SolidLine))
- qp.setBrush(QColor(200, 0, 0))
- qp.drawRect(430, 15, 200, 100)
-
- def drawRectangles4(self, qp): # 绘制线条矩形2
- path = QtGui.QPainterPath()
- qp.setPen(QPen(Qt.blue, 2, Qt.SolidLine))
- qp.setBrush(QColor(0, 0, 0, 0)) # 设置画刷颜色透明
- path.addRect(100, 200, 200, 100)
- qp.drawPath(path)
diff --git a/app/components/scroll_bar.py b/app/components/scroll_bar.py
deleted file mode 100644
index b6c96aa..0000000
--- a/app/components/scroll_bar.py
+++ /dev/null
@@ -1,48 +0,0 @@
-from PyQt5.QtWidgets import QScrollBar
-
-
-class ScrollBar(QScrollBar):
- def __init__(self):
- super().__init__()
- self.setStyleSheet(
- '''
- QScrollBar:vertical {
- border-width: 0px;
- border: none;
- background:rgba(133, 135, 138, 0);
- width:4px;
- margin: 0px 0px 0px 0px;
- }
- QScrollBar::handle:vertical {
- background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
- stop: 0 rgb(133, 135, 138), stop: 0.5 rgb(133, 135, 138), stop:1 rgb(133, 135, 138));
- min-height: 20px;
- max-height: 20px;
- margin: 0 0px 0 0px;
- border-radius: 2px;
- }
- QScrollBar::add-line:vertical {
- background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
- stop: 0 rgba(133, 135, 138, 0), stop: 0.5 rgba(133, 135, 138, 0), stop:1 rgba(133, 135, 138, 0));
- height: 0px;
- border: none;
- subcontrol-position: bottom;
- subcontrol-origin: margin;
- }
- QScrollBar::sub-line:vertical {
- background: qlineargradient(x1:0, y1:0, x2:1, y2:0,
- stop: 0 rgba(133, 135, 138, 0), stop: 0.5 rgba(133, 135, 138, 0), stop:1 rgba(133, 135, 138, 0));
- height: 0 px;
- border: none;
- subcontrol-position: top;
- subcontrol-origin: margin;
- }
- QScrollBar::sub-page:vertical {
- background: rgba(133, 135, 138, 0);
- }
-
- QScrollBar::add-page:vertical {
- background: rgba(133, 135, 138, 0);
- }
- '''
- )
\ No newline at end of file
diff --git a/app/config.py b/app/config.py
deleted file mode 100644
index 901283a..0000000
--- a/app/config.py
+++ /dev/null
@@ -1,35 +0,0 @@
-import os
-
-version = '2.0.5'
-contact = '701805520'
-github = 'https://github.com/LC044/WeChatMsg'
-website = 'https://memotrace.cn/'
-copyright = '© 2022-2024 SiYuan'
-license = 'GPLv3'
-description = [
- '1. 支持获取个人信息
',
- '2. 支持显示聊天界面
',
- '3. 支持导出聊天记录
* csv
* html
* '
- 'txt
* docx
',
- '4. 生成年度报告——圣诞特别版',
-]
-about = f'''
- 版本:{version}
- QQ交流群:请关注微信公众号回复:联系方式
- 地址:{github}
- 官网:{website}
- 新特性:
{''.join(['' + i for i in description])}
- License {license}
- Copyright {copyright}
-'''
-
-# 数据存放文件路径
-INFO_FILE_PATH = './app/data/info.json' # 个人信息文件
-DB_DIR = './app/Database/Msg'
-OUTPUT_DIR = './data/' # 输出文件夹
-os.makedirs('./app/data', exist_ok=True)
-os.makedirs(DB_DIR, exist_ok=True)
-os.makedirs(OUTPUT_DIR, exist_ok=True)
-# 全局参数
-SEND_LOG_FLAG = True # 是否发送错误日志
-SERVER_API_URL = 'http://api.lc044.love' # api接口
\ No newline at end of file
diff --git a/app/data/__init__.py b/app/data/__init__.py
deleted file mode 100644
index 64d5f6a..0000000
--- a/app/data/__init__.py
+++ /dev/null
@@ -1,9 +0,0 @@
-# -*- coding: utf-8 -*-
-"""
-@File : __init__.py.py
-@Author : Shuaikang Zhou
-@Time : 2022/12/13 14:19
-@IDE : Pycharm
-@Version : Python3.10
-@comment : ···
-"""
diff --git a/app/decrypt/decrypt.py b/app/decrypt/decrypt.py
deleted file mode 100644
index f4c57f4..0000000
--- a/app/decrypt/decrypt.py
+++ /dev/null
@@ -1,208 +0,0 @@
-# -*- coding: utf-8 -*-#
-# -------------------------------------------------------------------------------
-# Name: getwxinfo.py
-# Description:
-# Author: xaoyaoo
-# Date: 2023/08/21
-# License: https://github.com/xaoyaoo/PyWxDump/blob/3b794bcb47b0457d1245ce5b4cfec61b74524073/LICENSE MIT
-# 微信数据库采用的加密算法是256位的AES-CBC。数据库的默认的页大小是4096字节即4KB,其中每一个页都是被单独加解密的。
-# 加密文件的每一个页都有一个随机的初始化向量,它被保存在每一页的末尾。
-# 加密文件的每一页都存有着消息认证码,算法使用的是HMAC-SHA1(安卓数据库使用的是SHA512)。它也被保存在每一页的末尾。
-# 每一个数据库文件的开头16字节都保存了一段唯一且随机的盐值,作为HMAC的验证和数据的解密。
-# 用来计算HMAC的key与解密的key是不同的,解密用的密钥是主密钥和之前提到的16字节的盐值通过PKCS5_PBKF2_HMAC1密钥扩展算法迭代64000次计算得到的。而计算HMAC的密钥是刚提到的解密密钥和16字节盐值异或0x3a的值通过PKCS5_PBKF2_HMAC1密钥扩展算法迭代2次计算得到的。
-# 为了保证数据部分长度是16字节即AES块大小的整倍数,每一页的末尾将填充一段空字节,使得保留字段的长度为48字节。
-# 综上,加密文件结构为第一页4KB数据前16字节为盐值,紧接着4032字节数据,再加上16字节IV和20字节HMAC以及12字节空字节;而后的页均是4048字节长度的加密数据段和48字节的保留段。
-# -------------------------------------------------------------------------------
-import argparse
-import hmac
-import hashlib
-import os
-from typing import Union, List
-from Cryptodome.Cipher import AES
-
-# from Crypto.Cipher import AES # 如果上面的导入失败,可以尝试使用这个
-
-SQLITE_FILE_HEADER = "SQLite format 3\x00" # SQLite文件头
-
-KEY_SIZE = 32
-DEFAULT_PAGESIZE = 4096
-DEFAULT_ITER = 64000
-
-
-# 通过密钥解密数据库
-def decrypt(key: str, db_path, out_path):
- """
- 通过密钥解密数据库
- :param key: 密钥 64位16进制字符串
- :param db_path: 待解密的数据库路径(必须是文件)
- :param out_path: 解密后的数据库输出路径(必须是文件)
- :return:
- """
- if not os.path.exists(db_path) or not os.path.isfile(db_path):
- return False, f"[-] db_path:'{db_path}' File not found!"
- if not os.path.exists(os.path.dirname(out_path)):
- return False, f"[-] out_path:'{out_path}' File not found!"
-
- if len(key) != 64:
- return False, f"[-] key:'{key}' Len Error!"
-
- password = bytes.fromhex(key.strip())
- with open(db_path, "rb") as file:
- blist = file.read()
-
- salt = blist[:16]
- byteKey = hashlib.pbkdf2_hmac("sha1", password, salt, DEFAULT_ITER, KEY_SIZE)
- first = blist[16:DEFAULT_PAGESIZE]
- if len(salt) != 16:
- return False, f"[-] db_path:'{db_path}' File Error!"
-
- mac_salt = bytes([(salt[i] ^ 58) for i in range(16)])
- mac_key = hashlib.pbkdf2_hmac("sha1", byteKey, mac_salt, 2, KEY_SIZE)
- hash_mac = hmac.new(mac_key, first[:-32], hashlib.sha1)
- hash_mac.update(b'\x01\x00\x00\x00')
-
- if hash_mac.digest() != first[-32:-12]:
- return False, f"[-] Key Error! (key:'{key}'; db_path:'{db_path}'; out_path:'{out_path}' )"
-
- newblist = [blist[i:i + DEFAULT_PAGESIZE] for i in range(DEFAULT_PAGESIZE, len(blist), DEFAULT_PAGESIZE)]
-
- with open(out_path, "wb") as deFile:
- deFile.write(SQLITE_FILE_HEADER.encode())
- t = AES.new(byteKey, AES.MODE_CBC, first[-48:-32])
- decrypted = t.decrypt(first[:-48])
- deFile.write(decrypted)
- deFile.write(first[-48:])
-
- for i in newblist:
- t = AES.new(byteKey, AES.MODE_CBC, i[-48:-32])
- decrypted = t.decrypt(i[:-48])
- deFile.write(decrypted)
- deFile.write(i[-48:])
- return True, [db_path, out_path, key]
-
-
-def batch_decrypt(key: str, db_path: Union[str, List[str]], out_path: str, is_logging: bool = False):
- if not isinstance(key, str) or not isinstance(out_path, str) or not os.path.exists(out_path) or len(key) != 64:
- error = f"[-] (key:'{key}' or out_path:'{out_path}') Error!"
- if is_logging: print(error)
- return False, error
-
- process_list = []
-
- if isinstance(db_path, str):
- if not os.path.exists(db_path):
- error = f"[-] db_path:'{db_path}' not found!"
- if is_logging: print(error)
- return False, error
-
- if os.path.isfile(db_path):
- inpath = db_path
- outpath = os.path.join(out_path, 'de_' + os.path.basename(db_path))
- process_list.append([key, inpath, outpath])
-
- elif os.path.isdir(db_path):
- for root, dirs, files in os.walk(db_path):
- for file in files:
- inpath = os.path.join(root, file)
- rel = os.path.relpath(root, db_path)
- outpath = os.path.join(out_path, rel, 'de_' + file)
-
- if not os.path.exists(os.path.dirname(outpath)):
- os.makedirs(os.path.dirname(outpath))
- process_list.append([key, inpath, outpath])
- else:
- error = f"[-] db_path:'{db_path}' Error "
- if is_logging: print(error)
- return False, error
-
- elif isinstance(db_path, list):
- rt_path = os.path.commonprefix(db_path)
- if not os.path.exists(rt_path):
- rt_path = os.path.dirname(rt_path)
-
- for inpath in db_path:
- if not os.path.exists(inpath):
- erreor = f"[-] db_path:'{db_path}' not found!"
- if is_logging: print(erreor)
- return False, erreor
-
- inpath = os.path.normpath(inpath)
- rel = os.path.relpath(os.path.dirname(inpath), rt_path)
- outpath = os.path.join(out_path, rel, 'de_' + os.path.basename(inpath))
- if not os.path.exists(os.path.dirname(outpath)):
- os.makedirs(os.path.dirname(outpath))
- process_list.append([key, inpath, outpath])
- else:
- error = f"[-] db_path:'{db_path}' Error "
- if is_logging: print(error)
- return False, error
-
- result = []
- for i in process_list:
- result.append(decrypt(*i)) # 解密
-
- # 删除空文件夹
- for root, dirs, files in os.walk(out_path, topdown=False):
- for dir in dirs:
- if not os.listdir(os.path.join(root, dir)):
- os.rmdir(os.path.join(root, dir))
-
- if is_logging:
- print("=" * 32)
- success_count = 0
- fail_count = 0
- for code, ret in result:
- if code == False:
- print(ret)
- fail_count += 1
- else:
- print(f'[+] "{ret[0]}" -> "{ret[1]}"')
- success_count += 1
- print("-" * 32)
- print(f"[+] 共 {len(result)} 个文件, 成功 {success_count} 个, 失败 {fail_count} 个")
- print("=" * 32)
- return True, result
-
-
-def encrypt(key: str, db_path, out_path):
- """
- 通过密钥加密数据库
- :param key: 密钥 64位16进制字符串
- :param db_path: 待加密的数据库路径(必须是文件)
- :param out_path: 加密后的数据库输出路径(必须是文件)
- :return:
- """
- if not os.path.exists(db_path) or not os.path.isfile(db_path):
- return False, f"[-] db_path:'{db_path}' File not found!"
- if not os.path.exists(os.path.dirname(out_path)):
- return False, f"[-] out_path:'{out_path}' File not found!"
-
- if len(key) != 64:
- return False, f"[-] key:'{key}' Len Error!"
-
- password = bytes.fromhex(key.strip())
- with open(db_path, "rb") as file:
- blist = file.read()
-
- salt = os.urandom(16) # 生成随机盐值
- byteKey = hashlib.pbkdf2_hmac("sha1", password, salt, DEFAULT_ITER, KEY_SIZE)
-
- # 计算消息认证码
- mac_salt = bytes([(salt[i] ^ 58) for i in range(16)])
- mac_key = hashlib.pbkdf2_hmac("sha1", byteKey, mac_salt, 2, KEY_SIZE)
- hash_mac = hmac.new(mac_key, blist[:-32], hashlib.sha1)
- hash_mac.update(b'\x01\x00\x00\x00')
- mac_digest = hash_mac.digest()
-
- newblist = [blist[i:i + DEFAULT_PAGESIZE] for i in range(DEFAULT_PAGESIZE, len(blist), DEFAULT_PAGESIZE)]
-
- with open(out_path, "wb") as enFile:
- enFile.write(salt) # 写入盐值
- enFile.write(mac_digest) # 写入消息认证码
-
- for i in newblist:
- t = AES.new(byteKey, AES.MODE_CBC, os.urandom(16)) # 生成随机的初始向量
- encrypted = t.encrypt(i) # 加密数据块
- enFile.write(encrypted)
-
- return True, [db_path, out_path, key]
\ No newline at end of file
diff --git a/app/decrypt/get_wx_info.py b/app/decrypt/get_wx_info.py
deleted file mode 100644
index 6d1bc2e..0000000
--- a/app/decrypt/get_wx_info.py
+++ /dev/null
@@ -1,468 +0,0 @@
-# -*- coding: utf-8 -*-#
-# -------------------------------------------------------------------------------
-# Name: getwxinfo.py
-# Description:
-# Author: xaoyaoo
-# Date: 2023/08/21
-# -------------------------------------------------------------------------------
-import hmac
-import hashlib
-import ctypes
-import winreg
-import pymem
-from win32com.client import Dispatch
-import psutil
-
-ReadProcessMemory = ctypes.windll.kernel32.ReadProcessMemory
-void_p = ctypes.c_void_p
-
-import binascii
-import pymem.process
-from pymem import Pymem
-from win32api import GetFileVersionInfo, HIWORD, LOWORD
-
-"""
-class Wechat来源:https://github.com/SnowMeteors/GetWeChatKey
-"""
-
-
-class Wechat:
- def __init__(self, pm):
- module = pymem.process.module_from_name(pm.process_handle, "WeChatWin.dll")
- self.pm = pm
- self.dllBase = module.lpBaseOfDll
- self.sizeOfImage = module.SizeOfImage
- self.bits = self.GetPEBits()
-
- # 通过解析PE来获取位数
- def GetPEBits(self):
- address = self.dllBase + self.pm.read_int(self.dllBase + 60) + 4 + 16
- SizeOfOptionalHeader = self.pm.read_short(address)
-
- # 0XF0 64bit
- if SizeOfOptionalHeader == 0xF0:
- return 64
-
- return 32
-
- def GetInfo(self):
- version = self.GetVersion()
- if not version:
- print("Get WeChatWin.dll Failed")
- return
-
- print(f"WeChat Version:{version}")
- print(f"WeChat Bits: {self.bits}")
-
- keyBytes = b'-----BEGIN PUBLIC KEY-----\n...'
-
- # 从内存中查找 BEGIN PUBLIC KEY 的地址
- publicKeyList = pymem.pattern.pattern_scan_all(self.pm.process_handle, keyBytes, return_multiple=True)
- if len(publicKeyList) == 0:
- print("Failed to find PUBLIC KEY")
- return
-
- keyAddr = self.GetKeyAddr(publicKeyList)
- if keyAddr is None:
- print("Failed to find key")
- return
-
- keyLenOffset = 0x8c if self.bits == 32 else 0xd0
-
- for addr in keyAddr:
- try:
- keyLen = self.pm.read_uchar(addr - keyLenOffset)
- if self.bits == 32:
- key = self.pm.read_bytes(self.pm.read_int(addr - 0x90), keyLen)
- else:
- key = self.pm.read_bytes(self.pm.read_longlong(addr - 0xd8), keyLen)
-
- key = binascii.b2a_hex(key).decode()
- if self.CheckKey(key):
- print(f"key is {key}")
- return key
- except:
- pass
-
- print("Find the end of the key")
-
- @staticmethod
- def CheckKey(key):
- # 目前key位数是32位
- if key is None or len(key) != 64:
- return False
-
- return True
-
- # 内存搜索特征码
- @staticmethod
- def SearchMemory(parent, child):
- offset = []
- index = -1
-
- while True:
- index = parent.find(child, index + 1)
- if index == -1:
- break
- offset.append(index)
-
- return offset
-
- # 获取key的地址
- def GetKeyAddr(self, publicKeyList):
- # 存放真正的key地址
- keyAddr = []
-
- # 读取整个 WeChatWin.dll 的内容
- buffer = self.pm.read_bytes(self.dllBase, self.sizeOfImage)
-
- byteLen = 4 if self.bits == 32 else 8
- for publicKeyAddr in publicKeyList:
- keyBytes = publicKeyAddr.to_bytes(byteLen, byteorder="little", signed=True)
- offset = self.SearchMemory(buffer, keyBytes)
-
- if not offset or len(offset) == 0:
- continue
-
- offset[:] = [x + self.dllBase for x in offset]
- keyAddr += offset
-
- if len(keyAddr) == 0:
- return None
-
- return keyAddr
-
- # 获取微信版本
- def GetVersion(self):
- WeChatWindll_path = ""
- for m in list(self.pm.list_modules()):
- path = m.filename
- if path.endswith("WeChatWin.dll"):
- WeChatWindll_path = path
- break
-
- if not WeChatWindll_path:
- return False
-
- version = GetFileVersionInfo(WeChatWindll_path, "\\")
-
- msv = version['FileVersionMS']
- lsv = version['FileVersionLS']
- version = f"{str(HIWORD(msv))}.{str(LOWORD(msv))}.{str(HIWORD(lsv))}.{str(LOWORD(lsv))}"
-
- return version
-
-
-# 获取exe文件的位数
-def get_exe_bit(file_path):
- """
- 获取 PE 文件的位数: 32 位或 64 位
- :param file_path: PE 文件路径(可执行文件)
- :return: 如果遇到错误则返回 64
- """
- try:
- with open(file_path, 'rb') as f:
- dos_header = f.read(2)
- if dos_header != b'MZ':
- print('get exe bit error: Invalid PE file')
- return 64
- # Seek to the offset of the PE signature
- f.seek(60)
- pe_offset_bytes = f.read(4)
- pe_offset = int.from_bytes(pe_offset_bytes, byteorder='little')
-
- # Seek to the Machine field in the PE header
- f.seek(pe_offset + 4)
- machine_bytes = f.read(2)
- machine = int.from_bytes(machine_bytes, byteorder='little')
-
- if machine == 0x14c:
- return 32
- elif machine == 0x8664:
- return 64
- else:
- print('get exe bit error: Unknown architecture: %s' % hex(machine))
- return 64
- except IOError:
- print('get exe bit error: File not found or cannot be opened')
- return 64
-
-
-# 读取内存中的字符串(非key部分)
-def get_info_without_key(h_process, address, n_size=64):
- array = ctypes.create_string_buffer(n_size)
- if ReadProcessMemory(h_process, void_p(address), array, n_size, 0) == 0: return "None"
- array = bytes(array).split(b"\x00")[0] if b"\x00" in array else bytes(array)
- text = array.decode('utf-8', errors='ignore')
- return text.strip() if text.strip() != "" else "None"
-
-
-def pattern_scan_all(handle, pattern, *, return_multiple=False, find_num=100):
- next_region = 0
- found = []
- user_space_limit = 0x7FFFFFFF0000 if sys.maxsize > 2 ** 32 else 0x7fff0000
- while next_region < user_space_limit:
- try:
- next_region, page_found = pymem.pattern.scan_pattern_page(
- handle,
- next_region,
- pattern,
- return_multiple=return_multiple
- )
- except Exception as e:
- print(e)
- break
- if not return_multiple and page_found:
- return page_found
- if page_found:
- found += page_found
- if len(found) > find_num:
- break
- return found
-
-
-def get_info_wxid(h_process):
- find_num = 100
- addrs = pattern_scan_all(h_process, br'\\Msg\\FTSContact', return_multiple=True, find_num=find_num)
- wxids = []
- for addr in addrs:
- array = ctypes.create_string_buffer(80)
- if ReadProcessMemory(h_process, void_p(addr - 30), array, 80, 0) == 0: return "None"
- array = bytes(array) # .split(b"\\")[0]
- array = array.split(b"\\Msg")[0]
- array = array.split(b"\\")[-1]
- wxids.append(array.decode('utf-8', errors='ignore'))
- wxid = max(wxids, key=wxids.count) if wxids else "None"
- return wxid
-
-
-def get_info_filePath(wxid="all"):
- if not wxid:
- return "None"
- w_dir = "MyDocument:"
- is_w_dir = False
-
- try:
- key = winreg.OpenKey(winreg.HKEY_CURRENT_USER, r"Software\Tencent\WeChat", 0, winreg.KEY_READ)
- value, _ = winreg.QueryValueEx(key, "FileSavePath")
- winreg.CloseKey(key)
- w_dir = value
- is_w_dir = True
- except Exception as e:
- w_dir = "MyDocument:"
-
- if not is_w_dir:
- try:
- user_profile = os.environ.get("USERPROFILE")
- path_3ebffe94 = os.path.join(user_profile, "AppData", "Roaming", "Tencent", "WeChat", "All Users", "config",
- "3ebffe94.ini")
- with open(path_3ebffe94, "r", encoding="utf-8") as f:
- w_dir = f.read()
- is_w_dir = True
- except Exception as e:
- w_dir = "MyDocument:"
-
- if w_dir == "MyDocument:":
- try:
- # 打开注册表路径
- key = winreg.OpenKey(winreg.HKEY_CURRENT_USER,
- r"Software\Microsoft\Windows\CurrentVersion\Explorer\User Shell Folders")
- documents_path = winreg.QueryValueEx(key, "Personal")[0] # 读取文档实际目录路径
- winreg.CloseKey(key) # 关闭注册表
- documents_paths = os.path.split(documents_path)
- if "%" in documents_paths[0]:
- w_dir = os.environ.get(documents_paths[0].replace("%", ""))
- w_dir = os.path.join(w_dir, os.path.join(*documents_paths[1:]))
- # print(1, w_dir)
- else:
- w_dir = documents_path
- except Exception as e:
- profile = os.environ.get("USERPROFILE")
- w_dir = os.path.join(profile, "Documents")
-
- msg_dir = os.path.join(w_dir, "WeChat Files")
-
- if wxid == "all" and os.path.exists(msg_dir):
- return msg_dir
-
- filePath = os.path.join(msg_dir, wxid)
- return filePath if os.path.exists(filePath) else "None"
-
-
-def get_key(db_path, addr_len):
- def read_key_bytes(h_process, address, address_len=8):
- array = ctypes.create_string_buffer(address_len)
- if ReadProcessMemory(h_process, 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, void_p(address), key, 32, 0) == 0: return "None"
- key_bytes = bytes(key)
- return key_bytes
-
- def verify_key(key, wx_db_path):
- if not wx_db_path or wx_db_path.lower() == "none":
- return True
- KEY_SIZE = 32
- DEFAULT_PAGESIZE = 4096
- DEFAULT_ITER = 64000
- with open(wx_db_path, "rb") as file:
- blist = file.read(5000)
- salt = blist[:16]
- byteKey = hashlib.pbkdf2_hmac("sha1", key, salt, DEFAULT_ITER, KEY_SIZE)
- first = blist[16:DEFAULT_PAGESIZE]
-
- mac_salt = bytes([(salt[i] ^ 58) for i in range(16)])
- mac_key = hashlib.pbkdf2_hmac("sha1", byteKey, mac_salt, 2, KEY_SIZE)
- hash_mac = hmac.new(mac_key, first[:-32], hashlib.sha1)
- hash_mac.update(b'\x01\x00\x00\x00')
-
- if hash_mac.digest() != first[-32:-12]:
- return False
- return True
-
- phone_type1 = "iphone\x00"
- phone_type2 = "android\x00"
- phone_type3 = "ipad\x00"
-
- pm = pymem.Pymem("WeChat.exe")
- module_name = "WeChatWin.dll"
-
- MicroMsg_path = os.path.join(db_path, "MSG", "MicroMsg.db")
-
- type1_addrs = pm.pattern_scan_module(phone_type1.encode(), module_name, return_multiple=True)
- type2_addrs = pm.pattern_scan_module(phone_type2.encode(), module_name, return_multiple=True)
- type3_addrs = pm.pattern_scan_module(phone_type3.encode(), module_name, return_multiple=True)
- type_addrs = type1_addrs if len(type1_addrs) >= 2 else type2_addrs if len(type2_addrs) >= 2 else type3_addrs if len(
- type3_addrs) >= 2 else "None"
- # print(type_addrs)
- if type_addrs == "None":
- return "None"
- for i in type_addrs[::-1]:
- for j in range(i, i - 2000, -addr_len):
- key_bytes = read_key_bytes(pm.process_handle, j, addr_len)
- if key_bytes == "None":
- continue
- if db_path != "None" and verify_key(key_bytes, MicroMsg_path):
- return key_bytes.hex()
- return "None"
-
-
-# 读取微信信息(account,mobile,name,mail,wxid,key)
-def read_info(version_list, is_logging=False):
- wechat_process = []
- result = []
- error = ""
- for process in psutil.process_iter(['name', 'exe', 'pid']):
- if process.name() == 'WeChat.exe':
- wechat_process.append(process)
-
- if len(wechat_process) == 0:
- error = "[-] WeChat No Run"
- if is_logging: print(error)
- return -1
-
- for process in wechat_process:
- tmp_rd = {}
-
- tmp_rd['pid'] = process.pid
- tmp_rd['version'] = Dispatch("Scripting.FileSystemObject").GetFileVersion(process.exe())
-
- wechat_base_address = 0
- for module in process.memory_maps(grouped=False):
- if module.path and 'WeChatWin.dll' in module.path:
- wechat_base_address = int(module.addr, 16)
- break
- if wechat_base_address == 0:
- error = f"[-] WeChat WeChatWin.dll Not Found"
- if is_logging: print(error)
- return -1
-
- Handle = ctypes.windll.kernel32.OpenProcess(0x1F0FFF, False, process.pid)
-
- bias_list = version_list.get(tmp_rd['version'], None)
- if not isinstance(bias_list, list) or len(bias_list) <= 4:
- error = f"[-] WeChat Current Version Is Not Supported(maybe not get account,mobile,name,mail)"
- if is_logging: print(error)
- tmp_rd['account'] = "None"
- tmp_rd['mobile'] = "None"
- tmp_rd['name'] = "None"
- tmp_rd['mail'] = "None"
- return tmp_rd['version']
- else:
- name_baseaddr = wechat_base_address + bias_list[0]
- account__baseaddr = wechat_base_address + bias_list[1]
- mobile_baseaddr = wechat_base_address + bias_list[2]
- mail_baseaddr = wechat_base_address + bias_list[3]
- # key_baseaddr = wechat_base_address + bias_list[4]
-
- tmp_rd['account'] = get_info_without_key(Handle, account__baseaddr, 32) if bias_list[1] != 0 else "None"
- tmp_rd['mobile'] = get_info_without_key(Handle, mobile_baseaddr, 64) if bias_list[2] != 0 else "None"
- tmp_rd['name'] = get_info_without_key(Handle, name_baseaddr, 64) if bias_list[0] != 0 else "None"
- tmp_rd['mail'] = get_info_without_key(Handle, mail_baseaddr, 64) if bias_list[3] != 0 else "None"
-
- addrLen = get_exe_bit(process.exe()) // 8
-
- tmp_rd['wxid'] = get_info_wxid(Handle)
- tmp_rd['filePath'] = get_info_filePath(tmp_rd['wxid']) if tmp_rd['wxid'] != "None" else "None"
- tmp_rd['key'] = "None"
- tmp_rd['key'] = get_key(tmp_rd['filePath'], addrLen)
- if tmp_rd['key'] == 'None':
- wechat = Pymem("WeChat.exe")
- key = Wechat(wechat).GetInfo()
- if key:
- tmp_rd['key'] = key
- result.append(tmp_rd)
-
- if is_logging:
- print("=" * 32)
- if isinstance(result, str): # 输出报错
- print(result)
- else: # 输出结果
- for i, rlt in enumerate(result):
- for k, v in rlt.items():
- print(f"[+] {k:>8}: {v}")
- print(end="-" * 32 + "\n" if i != len(result) - 1 else "")
- print("=" * 32)
-
- return result
-
-
-import os
-import sys
-
-
-def resource_path(relative_path):
- """ Get absolute path to resource, works for dev and for PyInstaller """
- base_path = getattr(sys, '_MEIPASS', os.path.dirname(os.path.abspath(__file__)))
- return os.path.join(base_path, relative_path)
-
-
-def get_info(VERSION_LIST):
- result = read_info(VERSION_LIST, True) # 读取微信信息
- return result
-
-
-if __name__ == "__main__":
- import argparse
-
- parser = argparse.ArgumentParser()
- parser.add_argument("--vlfile", type=str, help="手机号", required=False)
- parser.add_argument("--vldict", type=str, help="微信昵称", required=False)
-
- args = parser.parse_args()
-
- # 读取微信各版本偏移
- if args.vlfile:
- VERSION_LIST_PATH = args.vlfile
- with open(VERSION_LIST_PATH, "r", encoding="utf-8") as f:
- VERSION_LIST = json.load(f)
- if args.vldict:
- VERSION_LIST = json.loads(args.vldict)
-
- if not args.vlfile and not args.vldict:
- VERSION_LIST_PATH = "../version_list.json"
-
- with open(VERSION_LIST_PATH, "r", encoding="utf-8") as f:
- VERSION_LIST = json.load(f)
-
- result = read_info(VERSION_LIST, True) # 读取微信信息
diff --git a/app/decrypt/version_list.json b/app/decrypt/version_list.json
deleted file mode 100644
index a56b00e..0000000
--- a/app/decrypt/version_list.json
+++ /dev/null
@@ -1,1011 +0,0 @@
-{
- "3.2.1.154": [
- 328121948,
- 328122328,
- 328123056,
- 328121976,
- 328123020,
- 0
- ],
- "3.3.0.115": [
- 31323364,
- 31323744,
- 31324472,
- 31323392,
- 31324436,
- 0
- ],
- "3.3.0.84": [
- 31315212,
- 31315592,
- 31316320,
- 31315240,
- 31316284,
- 0
- ],
- "3.3.0.93": [
- 31323364,
- 31323744,
- 31324472,
- 31323392,
- 31324436,
- 0
- ],
- "3.3.5.34": [
- 30603028,
- 30603408,
- 30604120,
- 30603056,
- 30604100,
- 0
- ],
- "3.3.5.42": [
- 30603012,
- 30603392,
- 30604120,
- 30603040,
- 30604084,
- 0
- ],
- "3.3.5.46": [
- 30578372,
- 30578752,
- 30579480,
- 30578400,
- 30579444,
- 0
- ],
- "3.4.0.37": [
- 31608116,
- 31608496,
- 31609224,
- 31608144,
- 31609188,
- 0
- ],
- "3.4.0.38": [
- 31604044,
- 31604424,
- 31605152,
- 31604072,
- 31605116,
- 0
- ],
- "3.4.0.50": [
- 31688500,
- 31688880,
- 31689608,
- 31688528,
- 31689572,
- 0
- ],
- "3.4.0.54": [
- 31700852,
- 31701248,
- 31700920,
- 31700880,
- 31701924,
- 0
- ],
- "3.4.5.27": [
- 32133788,
- 32134168,
- 32134896,
- 32133816,
- 32134860,
- 0
- ],
- "3.4.5.45": [
- 32147012,
- 32147392,
- 32147064,
- 32147040,
- 32148084,
- 0
- ],
- "3.5.0.20": [
- 35494484,
- 35494864,
- 35494536,
- 35494512,
- 35495556,
- 0
- ],
- "3.5.0.29": [
- 35507980,
- 35508360,
- 35508032,
- 35508008,
- 35509052,
- 0
- ],
- "3.5.0.33": [
- 35512140,
- 35512520,
- 35512192,
- 35512168,
- 35513212,
- 0
- ],
- "3.5.0.39": [
- 35516236,
- 35516616,
- 35516288,
- 35516264,
- 35517308,
- 0
- ],
- "3.5.0.42": [
- 35512140,
- 35512520,
- 35512192,
- 35512168,
- 35513212,
- 0
- ],
- "3.5.0.44": [
- 35510836,
- 35511216,
- 35510896,
- 35510864,
- 35511908,
- 0
- ],
- "3.5.0.46": [
- 35506740,
- 35507120,
- 35506800,
- 35506768,
- 35507812,
- 0
- ],
- "3.6.0.18": [
- 35842996,
- 35843376,
- 35843048,
- 35843024,
- 35844068,
- 0
- ],
- "3.6.5.7": [
- 35864356,
- 35864736,
- 35864408,
- 35864384,
- 35865428,
- 0
- ],
- "3.6.5.16": [
- 35909428,
- 35909808,
- 35909480,
- 35909456,
- 35910500,
- 0
- ],
- "3.7.0.26": [
- 37105908,
- 37106288,
- 37105960,
- 37105936,
- 37106980,
- 0
- ],
- "3.7.0.29": [
- 37105908,
- 37106288,
- 37105960,
- 37105936,
- 37106980,
- 0
- ],
- "3.7.0.30": [
- 37118196,
- 37118576,
- 37118248,
- 37118224,
- 37119268,
- 0
- ],
- "3.7.5.11": [
- 37883280,
- 37884088,
- 37883136,
- 37883008,
- 37884052,
- 0
- ],
- "3.7.5.23": [
- 37895736,
- 37896544,
- 37895592,
- 37883008,
- 37896508,
- 0
- ],
- "3.7.5.27": [
- 37895736,
- 37896544,
- 37895592,
- 37895464,
- 37896508,
- 0
- ],
- "3.7.5.31": [
- 37903928,
- 37904736,
- 37903784,
- 37903656,
- 37904700,
- 0
- ],
- "3.7.6.24": [
- 38978840,
- 38979648,
- 38978696,
- 38978604,
- 38979612,
- 0
- ],
- "3.7.6.29": [
- 38986376,
- 38987184,
- 38986232,
- 38986104,
- 38987148,
- 0
- ],
- "3.7.6.44": [
- 39016520,
- 39017328,
- 39016376,
- 38986104,
- 39017292,
- 0
- ],
- "3.8.0.31": [
- 46064088,
- 46064912,
- 46063944,
- 38986104,
- 46064876,
- 0
- ],
- "3.8.0.33": [
- 46059992,
- 46060816,
- 46059848,
- 38986104,
- 46060780,
- 0
- ],
- "3.8.0.41": [
- 46064024,
- 46064848,
- 46063880,
- 38986104,
- 46064812,
- 0
- ],
- "3.8.1.26": [
- 46409448,
- 46410272,
- 46409304,
- 38986104,
- 46410236,
- 0
- ],
- "3.9.0.28": [
- 48418376,
- 48419280,
- 48418232,
- 38986104,
- 48419244,
- 0
- ],
- "3.9.2.23": [
- 50320784,
- 50321712,
- 50320640,
- 38986104,
- 50321676,
- 50592864
- ],
- "3.9.2.26": [
- 50329040,
- 50329968,
- 50328896,
- 38986104,
- 50329932,
- 0
- ],
- "3.9.5.81": [
- 61650872,
- 61652208,
- 61650680,
- 0,
- 61652144,
- 0
- ],
- "3.9.5.91": [
- 61654904,
- 61656240,
- 61654712,
- 38986104,
- 61656176,
- 61677112
- ],
- "3.9.6.19": [
- 61997688,
- 61997464,
- 61997496,
- 38986104,
- 61998960,
- 0
- ],
- "3.9.6.33": [
- 62030600,
- 62031936,
- 62030408,
- 0,
- 62031872,
- 0
- ],
- "3.9.7.15": [
- 63482696,
- 63484032,
- 63482504,
- 0,
- 63483968,
- 0
- ],
- "3.9.7.25": [
- 63482760,
- 63484096,
- 63482568,
- 0,
- 63484032,
- 0
- ],
- "3.9.7.29": [
- 63486984,
- 63488320,
- 63486792,
- 0,
- 63488256,
- 63488352
- ],
- "3.9.8.15": [
- 64996632,
- 64997968,
- 64996440,
- 0,
- 64997904,
- 65011632
- ],
- "3.9.8.25": [
- 65000920,
- 0,
- 65000728,
- 0,
- 0,
- 0
- ],
- "3.9.9.27": [
- 68065304,
- 0,
- 68065112,
- 0,
- 68066576
- ],
- "3.9.9.35": [
- 68065304,
- 0,
- 68065112,
- 0,
- 68066576
- ],
- "3.9.9.148": [
- 69043224,
- 69044560,
- 69043032,
- 0,
- 0
- ],
- "3.9.5.65": [
- 61642744,
- 61644080,
- 61644081,
- 0,
- 61644016
- ],
- "3.9.9.38": [
- 68070104,
- 68071440,
- 68069912,
- 0,
- 68071376
- ],
- "3.9.9.147": [
- 76731445,
- 0,
- 69043032,
- 0,
- 0
- ],
- "3.6.0.0": [
- 35842996,
- 35844104,
- 35843048,
- 0,
- 0
- ],
- "3.9.9.43": [
- 68065944,
- 68067280,
- 68065752,
- 0,
- 68067216
- ],
- "3.9.9.143": [
- 69035096,
- 69036432,
- 69034904,
- 0,
- 69036368
- ],
- "3.3.0.0": [
- 31082900,
- 31083928,
- 31082952,
- 0,
- 0
- ],
- "3.9.9.25": [
- 68065304,
- 0,
- 68065112,
- 0,
- 68066576
- ],
- "3.9.8.11": [
- 0,
- 0,
- 0,
- 0,
- 64998160
- ],
- "3.9.9.16": [
- 68060952,
- 0,
- 0,
- 0,
- 68062224
- ],
- "3.9.6.24": [
- 62022264,
- 62044248,
- 62022072,
- 0,
- 62023536
- ],
- "3.9.9.34": [
- 56102304,
- 56118252,
- 56102160,
- 0,
- 0
- ],
- "3.9.6.39": [
- 0,
- 63280064,
- 63278536,
- 0,
- 63280000
- ],
- "3.9.7.28": [
- 52433824,
- 52434792,
- 52434793,
- 0,
- 0
- ],
- "3.9.8.137": [
- 65894869,
- 65909760,
- 65908232,
- 0,
- 65909696
- ],
- "3.9.5.25": [
- 61530888,
- 0,
- 61530696,
- 0,
- 61532160
- ],
- "3.9.5.80": [
- 50681672,
- 50682640,
- 50682641,
- 0,
- 0
- ],
- "3.7.6.45": [
- 39016456,
- 0,
- 39016312,
- 0,
- 0
- ],
- "3.5.0.0": [
- 35507848,
- 35507848,
- 35506800,
- 0,
- 0
- ],
- "3.9.8.9": [
- 65001112,
- 65002448,
- 0,
- 0,
- 65002384
- ],
- "3.9.6.47": [
- 0,
- 63288512,
- 63286984,
- 0,
- 0
- ],
- "3.9.8.8": [
- 64996952,
- 0,
- 64996760,
- 0,
- 64998224
- ],
- "3.9.6.43": [
- 63283144,
- 63284480,
- 63282952,
- 0,
- 0
- ],
- "3.9.0.22": [
- 48405896,
- 48406800,
- 48405752,
- 0,
- 0
- ],
- "3.7.6.43": [
- 39016456,
- 39261580,
- 39016312,
- 0,
- 0
- ],
- "3.0.0.0": [
- 25834908,
- 25846332,
- 25834960,
- 0,
- 0
- ],
- "3.7.0.23": [
- 37105844,
- 37106952,
- 37105896,
- 0,
- 0
- ],
- "3.9.6.8": [
- 61960264,
- 61961600,
- 0,
- 0,
- 61961536
- ],
- "3.9.5.55": [
- 61589448,
- 61590784,
- 61589256,
- 0,
- 61590720
- ],
- "3.9.8.27": [
- 65000920,
- 65002256,
- 65000728,
- 0,
- 65002192
- ],
- "2.4.5.1": [
- 27329425,
- 24589520,
- 24588544,
- 0,
- 0
- ],
- "3.9.6.37": [
- 63287160,
- 63310040,
- 63286968,
- 0,
- 63288432
- ],
- "3.9.5.77": [
- 61652272,
- 61652272,
- 61650744,
- 0,
- 61652208
- ],
- "3.9.9.13": [
- 68044888,
- 68069384,
- 68044696,
- 0,
- 68046160
- ],
- "3.9.5.39": [
- 0,
- 0,
- 0,
- 0,
- 61565216
- ],
- "3.9.6.17": [
- 61972568,
- 61973904,
- 61972376,
- 0,
- 61973840
- ],
- "3.9.9.11": [
- 0,
- 0,
- 68032344,
- 0,
- 68033808
- ],
- "3.2.1.0": [
- 0,
- 28123056,
- 28122080,
- 0,
- 0
- ],
- "3.8.1.25": [
- 0,
- 0,
- 46405272,
- 0,
- 0
- ],
- "3.9.6.29": [
- 0,
- 62052520,
- 62030344,
- 0,
- 62031808
- ],
- "3.9.8.12": [
- 53479320,
- 53480288,
- 53479176,
- 0,
- 0
- ],
- "3.9.5.73": [
- 61650872,
- 61652208,
- 0,
- 0,
- 61652144
- ],
- "3.9.1.25": [
- 48634640,
- 48635544,
- 48634496,
- 0,
- 0
- ],
- "3.8.0.29": [
- 50910714,
- 0,
- 46060689,
- 0,
- 0
- ],
- "3.9.9.22": [
- 56102304,
- 0,
- 56102160,
- 0,
- 0
- ],
- "3.9.10.10": [
- 102868520,
- 102869856,
- 102868328,
- 0,
- 102869792
- ],
- "3.8.0.18": [
- 46043224,
- 46044048,
- 46043080,
- 0,
- 0
- ],
- "3.9.10.13": [
- 0,
- 0,
- 95125416,
- 0,
- 0
- ],
- "3.9.10.18": [
- 95129640,
- 95130976,
- 95129448,
- 0,
- 0
- ],
- "3.9.10.11": [
- 102868456,
- 102869792,
- 102868264,
- 0,
- 0
- ],
- "3.9.7.14": [
- 0,
- 0,
- 63478408,
- 0,
- 63479872
- ],
- "3.9.10.19": [
- 95129768,
- 0,
- 95129576,
- 0,
- 95131192
- ],
- "3.7.6.38": [
- 39016136,
- 0,
- 39015992,
- 0,
- 0
- ],
- "3.9.10.9": [
- 102868712,
- 102870048,
- 102868520,
- 0,
- 102869984
- ],
- "3.9.10.16": [
- 71305680,
- 71306648,
- 71305536,
- 0,
- 0
- ],
- "3.9.0.26": [
- 48405896,
- 0,
- 48405752,
- 0,
- 0
- ],
- "3.9.7.13": [
- 63474296,
- 63475632,
- 63474104,
- 0,
- 63475568
- ],
- "3.8.0.15": [
- 46014616,
- 0,
- 46014472,
- 0,
- 0
- ],
- "3.9.10.25": [
- 95135256,
- 95136592,
- 95135064,
- 0,
- 95136528
- ],
- "3.9.10.27": [
- 95125656,
- 95126992,
- 95125464,
- 0,
- 95126928
- ],
- "3.8.0.27": [
- 50268814,
- 46060496,
- 46060497,
- 0,
- 0
- ],
- "3.9.10.26": [
- 71305744,
- 71306712,
- 71305600,
- 0,
- 0
- ],
- "3.9.11.7": [
- 93459160,
- 93460496,
- 93458968,
- 0,
- 0
- ],
- "3.9.11.5": [
- 93456272,
- 93456272,
- 93454744,
- 0,
- 0
- ],
- "3.9.11.8": [
- 93454872,
- 93456208,
- 93454680,
- 0,
- 93456144
- ],
- "3.9.11.9": [
- 93463000,
- 93464336,
- 93462808,
- 0,
- 93464272
- ],
- "3.9.11.13": [
- 93546264,
- 93547600,
- 93547603,
- 0,
- 93547536
- ],
- "3.9.11.15": [
- 93550360,
- 93551696,
- 93550168,
- 0,
- 93551632
- ],
- "3.9.11.17": [
- 93550360,
- 0,
- 93550168,
- 0,
- 93551632
- ],
- "3.9.6.13": [
- 0,
- 0,
- 0,
- 0,
- 61965584
- ],
- "3.9.6.124": [
- 62034888,
- 0,
- 62034696,
- 0,
- 62036160
- ],
- "3.9.11.19": [
- 93550296,
- 93551632,
- 93550104,
- 0,
- 93551568
- ],
- "3.9.11.16": [
- 69861360,
- 69862328,
- 69861216,
- 0,
- 0
- ],
- "3.9.11.18": [
- 69861296,
- 69862264,
- 69861152,
- 0,
- 0
- ],
- "3.9.11.23": [
- 93701208,
- 93725072,
- 93701016,
- 0,
- 0
- ],
- "3.7.5.25": [
- 37899832,
- 37900640,
- 37899688,
- 0,
- 0
- ],
- "3.9.11.25": [
- 93701080,
- 93702416,
- 93700888,
- 0,
- 0
- ],
- "3.9.11.24": [
- 69992304,
- 69993272,
- 0,
- 0,
- 0
- ],
- "3.9.0.18": [
- 48385208,
- 48386112,
- 48385064,
- 0,
- 0
- ],
- "3.9.7.24": [
- 0,
- 52434792,
- 52433680,
- 0,
- 30689842
- ],
- "3.9.12.4": [
- 93809448,
- 0,
- 93809256,
- 0,
- 93810720
- ],
- "3.9.12.9": [
- 93815072,
- 93815072,
- 93813544,
- 0,
- 93815008
- ],
- "3.9.12.11": [
- 93810784,
- 93810784,
- 93809256,
- 0,
- 93810720
- ],
- "3.9.11.33": [
- 0,
- 93702416,
- 0,
- 0,
- 93702352
- ],
- "3.9.12.15": [93813544, 0, 93813352, 0, 93814968],
- "3.9.12.14": [
- 70045856,
- 70046824,
- 70045712,
- 0,
- 0
- ]
-}
\ No newline at end of file
diff --git a/app/log/__init__.py b/app/log/__init__.py
deleted file mode 100644
index 77076b1..0000000
--- a/app/log/__init__.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from .logger import log, logger
-
-__all__ = ["logger", "log"]
diff --git a/app/log/exception_handling.py b/app/log/exception_handling.py
deleted file mode 100644
index c279ca4..0000000
--- a/app/log/exception_handling.py
+++ /dev/null
@@ -1,75 +0,0 @@
-import sqlite3
-import sys
-import traceback
-
-import requests
-
-from app.person import Me
-
-
-class ExceptionHanding:
- def __init__(self, exc_type, exc_value, traceback_):
- self.exc_type = exc_type
- self.exc_value = exc_value
- self.traceback = traceback_
- self.error_message = ''.join(traceback.format_exception(exc_type, exc_value, traceback_))
-
- def parser_exc(self):
- if isinstance(self.exc_value, PermissionError):
- return f'权限错误,请使用管理员身份运行并将文件夹设置为可读写'
- elif isinstance(self.exc_value, sqlite3.DatabaseError):
- return '数据库错误,请删除app文件夹后重启电脑再运行软件'
- elif isinstance(self.exc_value, OSError) and self.exc_value.errno == 28:
- return '空间磁盘不足,请预留足够多的磁盘空间以供软件正常运行'
- elif isinstance(self.exc_value, TypeError) and 'NoneType' in str(self.exc_value) and 'not iterable' in str(
- self.exc_value):
- return '数据库错误,请删除app文件夹后重启电脑再运行软件'
- elif isinstance(self.exc_value,KeyboardInterrupt):
- return ''
- else:
- return '未知错误类型,可参考 https://blog.lc044.love/post/7 解决该问题\n温馨提示:重启电脑可解决80%的问题'
-
- def __str__(self):
- errmsg = f'{self.error_message}\n{self.parser_exc()}'
- return errmsg
-
-
-def excepthook(exc_type, exc_value, traceback_):
- # 将异常信息转为字符串
-
- # 在这里处理全局异常
-
- error_message = ExceptionHanding(exc_type, exc_value, traceback_)
- txt = '您可添加QQ群发送log文件以便解决该问题'
- msg = f"Exception Type: {exc_type.__name__}\nException Value: {exc_value}\ndetails: {error_message}\n\n{txt}"
- print(msg)
-
- # 调用原始的 excepthook,以便程序正常退出
- sys.__excepthook__(exc_type, exc_value, traceback_)
-
-def send_error_msg( message):
- url = "http://api.lc044.love/error"
- if not message:
- return {
- 'code': 201,
- 'errmsg': '日志为空'
- }
- data = {
- 'username': Me().wxid,
- 'error': message
- }
- try:
- response = requests.post(url, json=data)
- if response.status_code == 200:
- resp_info = response.json()
- return resp_info
- else:
- return {
- 'code': 503,
- 'errmsg': '服务器错误'
- }
- except:
- return {
- 'code': 404,
- 'errmsg': '客户端错误'
- }
diff --git a/app/person.py b/app/person.py
deleted file mode 100644
index 56d0e5e..0000000
--- a/app/person.py
+++ /dev/null
@@ -1,152 +0,0 @@
-"""
-定义各种联系人
-"""
-import json
-import os.path
-import re
-from typing import Dict
-from PyQt5.QtGui import QPixmap
-
-from app.config import INFO_FILE_PATH
-from app.ui.Icon import Icon
-
-
-def singleton(cls):
- _instance = {}
-
- def inner():
- if cls not in _instance:
- _instance[cls] = cls()
- return _instance[cls]
-
- return inner
-
-
-class Person:
- def __init__(self):
- self.avatar_path = None
- self.avatar = None
- self.avatar_path_qt = Icon.Default_avatar_path
- self.detail = {}
-
- def set_avatar(self, img_bytes):
- if not img_bytes:
- self.avatar.load(Icon.Default_avatar_path)
- return
- if img_bytes[:4] == b'\x89PNG':
- self.avatar.loadFromData(img_bytes, format='PNG')
- else:
- self.avatar.loadFromData(img_bytes, format='jfif')
-
- def save_avatar(self, path=None):
- if not self.avatar:
- return
- if path:
- save_path = path
- if os.path.exists(save_path):
- self.avatar_path = save_path
- return save_path
- else:
- os.makedirs('./data/avatar', exist_ok=True)
- save_path = os.path.join(f'data/avatar/', self.wxid + '.png')
- self.avatar_path = save_path
- if not os.path.exists(save_path):
- self.avatar.save(save_path)
- print('保存头像', save_path)
-
-
-@singleton
-class Me(Person):
- def __init__(self):
- super().__init__()
- self.avatar = QPixmap(Icon.Default_avatar_path)
- self.avatar_path = ':/icons/icons/default_avatar.svg'
- self.wxid = 'wxid_00112233'
- self.wx_dir = ''
- self.name = ''
- self.mobile = ''
- self.smallHeadImgUrl = ''
- self.nickName = self.name
- self.remark = self.nickName
- self.token = ''
-
- def save_info(self):
- if os.path.exists(INFO_FILE_PATH):
- with open(INFO_FILE_PATH, 'r', encoding='utf-8') as f:
- info_data = json.loads(f.read())
- info_data['name'] = self.name
- info_data['mobile'] = self.mobile
- with open(INFO_FILE_PATH, 'w', encoding='utf-8') as f:
- json.dump(info_data, f, ensure_ascii=False, indent=4)
-
-class Contact(Person):
- def __init__(self, contact_info: Dict):
- super().__init__()
- self.wxid = contact_info.get('UserName')
- self.remark = contact_info.get('Remark')
- # Alias,Type,Remark,NickName,PYInitial,RemarkPYInitial,ContactHeadImgUrl.smallHeadImgUrl,ContactHeadImgUrl,bigHeadImgUrl
- self.alias = contact_info.get('Alias')
- self.nickName = contact_info.get('NickName')
- if not self.remark:
- self.remark = self.nickName
- self.remark = re.sub(r'[\\/:*?"<>|\s\.]', '_', self.remark)
- self.smallHeadImgUrl = contact_info.get('smallHeadImgUrl')
- self.smallHeadImgBLOG = b''
- self.avatar = QPixmap()
- self.avatar_path = Icon.Default_avatar_path
- self.is_chatroom = self.wxid.__contains__('@chatroom')
- self.detail: Dict = contact_info.get('detail')
- self.label_name = contact_info.get('label_name') # 联系人的标签分类
-
- """
- detail存储了联系人的详细信息,是个字典
- {
- 'region': tuple[国家,省份,市], # 地区三元组
- 'signature': str, # 个性签名
- 'telephone': str, # 电话号码,自己写的备注才会显示
- 'gender': int, # 性别 0:未知,1:男,2:女
- }
- """
-
-
-class ContactDefault(Person):
- def __init__(self, wxid=""):
- super().__init__()
- self.avatar = QPixmap(Icon.Default_avatar_path)
- self.avatar_path = ':/icons/icons/default_avatar.svg'
- self.wxid = wxid
- self.remark = wxid
- self.alias = wxid
- self.nickName = wxid
- self.smallHeadImgUrl = ""
- self.smallHeadImgBLOG = b''
- self.is_chatroom = False
- self.detail = {}
-
-
-class Contacts:
- def __init__(self):
- self.contacts: Dict[str:Contact] = {}
-
- def add(self, wxid, contact: Contact):
- if wxid not in contact:
- self.contacts[wxid] = contact
-
- def get(self, wxid: str) -> Contact:
- return self.contacts.get(wxid)
-
- def remove(self, wxid: str):
- return self.contacts.pop(wxid)
-
- def save_avatar(self, avatar_dir: str = './data/avatar/'):
- for wxid, contact in self.contacts.items():
- avatar_path = os.path.join(avatar_dir, wxid + '.png')
- if os.path.exists(avatar_path):
- continue
- contact.save_avatar(avatar_path)
-
-
-if __name__ == '__main__':
- p1 = Me()
- p2 = Me()
- print(p1 == p2)
diff --git a/app/resources/__init__.py b/app/resources/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/app/resources/data/file.png b/app/resources/data/file.png
deleted file mode 100644
index 565cf36d3ce1a2950988b296684b68e897a27199..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 568
zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=oCO|{#S9F5M?jcysy3fA0|VnL
zPZ!6KiaBp*`)4s3inPanI`&*%#Bs)&rQsh~Y`ystwR3a5`!8-TS-q&EgIhfAK(Dl*
zY@u50lxe#%4d;GbS$W^?;dFg{V^0N!CIt?;#YV5pUwr?4eQA*9i535C1um`&TOGOh
z;tbhaX59@}-j>O~`ueMOV+706O+|ao7wxQJZE$DU(7wexnxWKcZka@z?%L8*vH$%h
zsc_k8&+=vXTD5nZ;j-2a0hWUo)Bf8pQEgyrJ{T;~R^IBwI7x-k#b@Q~98YHk-uA=k
z$!Buy=Cdm`X>>VgK6PcikYRF5+D7nT*=}FeJBB~{8KnA;>mQr(?D~V^oiYlKBG3F0
zU&oYTG;`0DtgUG>$2Y{SSL{-9aJdi_zlXteu3!GDt69a{Z#xRG+;84?t!b-RhsM_h
zkq=cC1g(sjdiv>J{=1fEfBDau)^P1tPF-<*;hnd)I3Mk0c&=KIm3{B)n}@Zm9SrXn
z?jQ&Y`F8(Y`3GI!MHQHKFc&C;h{t;yx-I#3KSEYqq5mMsuenJ3O$WyVz5~4o;ycq5
zy9R3pb0q|EU}0~lm+Tj<#}XN9b!C1;u^h;iPk8?M=k27875$HYsF*Q#WSGxB%Ok_W
e(WFpumVHl5O59?hi(7$7fx*+&&t;ucLK6UVCGySy
diff --git a/app/resources/data/icons/csv.png b/app/resources/data/icons/csv.png
deleted file mode 100644
index 28b84e21b45dd71616d77a0db943a5fd87f0cea4..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 6549
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
O!es`&ZGtt1$NmpFZSdA`o;+}Ax%u3DQP*azOn
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
diff --git a/app/resources/data/icons/pause.png b/app/resources/data/icons/pause.png
deleted file mode 100644
index 5cdcf61ccb1762cfc6159ce23c1305069167d1bf..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 1684
zcmV;F25b3=P)BOgQB-%G;Gy?1^mGn2XZoJ&|CjvYJpd`nA9A2aU)&;?*KfOY^I
zWRu4L9s&3rz?$nx;9COeXKI#Pw-8ogUZXAz
z1rmuw`~Xe>I0!(ATb4Bg078hid_LcyD9Tm<+lgqOyn7zm
z-vV$Iz?boOJhQ5SL?V%m08RnucjN&~F!Q&wv$I#m#>O7iGhQGNXm4$8Jw!x@0rcVD
zpvD25j>qGF)jC$qfrLV#BSMJJ0ki?g6H#h@e*WC(=;&|t$X7zw_0FcIrsK@40Z;+V
zSeEtK$jHdowe+tV$iTor^W5Cr5D~o#0L(logcyv)VpGdgFHbZY-Cs|7C
zy@O}Zo_(@ZKV_+tnx?g>s`?cXy$+zj%)_}{?xSQf`S%LCFCd*xKj`Y}`r7ODT12#u
zh;|l>#g{ufJHMZrnwqcFui_m!di3aqmX?-p0PF&gW9G177(cCsd?mu+a4!)V0DJ(l
zrfI%+;lhQ-<=U1FWME*R`N@+f=K&l5aKBhAzMn`W?yjD4WrD%rRz*=R0(c(4wdUsL
z56V4eVsTesZf4od
zD!W!ClgZ@v?AbF3;4l&G?CI&bpUGrym9|+BNF);3NJQrWG%@pVJRVQ2rSMXu)9F9`
ze!n21ULxARd-v{3>2!L=IsfdVbqYWmGf(Dnxif34TAf@jcZQiKVSmd`m7aSVh@5ux
z1IP;@22-h2acwoLlS-wELWn^Cc>w*=8Nk9jasmK|DAnl4E*K2Hsq1=YwRXC$cLsyO
zHygP|EEbz0BIl7m;T#u$J&T1s0W8eV&!21L0^xADTTzsRs;a})+N!ELtSCw%91eFk
zaxLjt0B}&c+kp^Kzx;io(Ycclq6Gk$xusfLr;XF6UZmp$03qG&K;(J?%=~R51G|Ca
z>bMJC*Ehjl6cuJ?XRo*^R}a#)0AQQ0>zjnfWE@p$$N
zB69%V%R7p&YK17lnqo>opw*D
zF-n}<1!UgI$#$$KN}M;8U5~6s1ndkTgb-~myVOmI^J4)!jfs3d-{GE8W0W{Q60mcz
zkQu;rMTzt80)Ufm-R_dla&VmQ2_nh@01@qTQK%stXClf9(=;alfSGp%0)cjS)oLdY
z2(-&2I84)=5VAgT2Y{!wwe^s@dbJ~6Jpk?`lgaze!s@d8dDu<4dT?AFcV}TW4!b_l
zSIe5a9F}F7a$vIt3A40My-3GC0G4#O1Cf=Ys{lMrO-;ue8Qd_8NoEcK_@svBpD=UC
zFpSAYt|c8k0IoV!uBW+QnOgY{cJ1S>)hZh-muNJ)gPFgFT|;@>sct=;YsmxS094De
zhBQrEvgF_fnx-k1Wevd&V!X6O_w1zcG=Lc*>h}43C)ZN47JNS6Nm&4zf$jQ~1%LH)
zIz7|V)AN9c-XNlVe!u^hOeXWkTFO>J)=Gu|2+OiQH4J0H(tOe5lgVUm`ThPEiD)Mg
zz24p3{lkqLHy*B;aT$Wa;8r2TMF6eLd}(B4gzV*Y!;vk7vJ3idWru#>~GK3WXm>
eM@Ls==I{@g`?|lZIZ3(z0000Mnxcq
z6zK?g0Rx7r6d|F_#cyWtoBuPv=0~0<&pCVVwf5TUoSWQ)>lUUgr}$4%QBkp&T{ePH
zQBi}AJ`8lgN_f=s5GpEeX)~isH^VYlrdR{4hO^%N+Hhsl%3@hveagZpO+s$Gi47AypX%
z+gP?s4)tbJJRHfR1cjXmm8%xhIg!(C3t4OK^W9B>%nBOYQEl7U$Tjfmhn|(?nf+fg
zGzSX5a=KPe6n>`R6OnQ`8$>S@{=Lfk%Nw=b64|PIMm#SAq{BZ}Hnlc4s}JiaWLb=M
z->#>AQdrtBa>Xv%sd+0eUjD3YcY3m^kDs)NmPXoQmiwTIAuU%Hoi}CCYCcxZu6f$A
z{otR3;03=F1@Sz{x>WQ0;^N83P`J84O19_yGj0f!?hc`%Jt}f4C7!+FyLop(3BPRV
z;+1B&MkgwXW79XFVADH1YO_0#WcGAILv9u!HUbjg&~>Q
z#ockq4!B}Bn}8Z>4$ve_I-gtbJZ@2Iw6t+S5#WaeV}??gc)nezZwf~
z(jz;6ow<*a;Or!0F0(@11l%%s)KrP4HOU_{wCmqGurUZ<%?_hDG`7nmN+B-Cc7)sf
zyef)=*n=GGrNfPis$18oT?Tmt-f=#n1G87mnstjSJ#&%SG8$k6s<0VjhWbPf`&e^!
zI-7E;{JnS6K<{6A#9BGNMCwub8*ziSdbaeUcQB|CsI10?;|C1*^5Y<6VWd;jyGbJj&%3
zhng!O=TEaf5{EL_RwGcFlarA-ejcyvw|{=4uHv_8g`Z4ezcx*6S@USi+AZL~t
zIp`r2G|v|!ZRq*{Ch8GL5B8lq&3fwym;-@&3)R#D+5?+oxCZ!-T1n>m+dYa509#2p
z=wed_c58*RvgLP<>tkvF0{Ia}>KObs;~a35efua|@F<%f)FC8%z;*Oh5Y*8z81ods
zG-E}I*~S1psfZv@Dz*wy3#$pmgpl2`nlm7vKA8hF+%VIwIr6#nt>*Vc6eAB}{st!1
z5hS!OBn)89+hJ~Y4IC-{0tc}hDc|qNb02<|?tJ}d&l_{|F)x@CPc}mh2pemaNL$e)4)j>`H%2Z?r4!q{o{T18kR{b3qSH%zl
zwb}du(guA4@J5{hS(P3E+oI{fX}9YDJZn4-;$L}dHxRtPR|;ubO*#q_23eW^;UM=A
zhK3{@K^rh4C&IukyMZZ|lZ)_c{n
zL55WT1Gym>NmBdTh(c?NA34efpRW#}@^}H~r>F6cG(16=RR2Gue&8T$Z6jXUuY!lI
z(zP3BoZJ)#fM7K*AJpqzf&yO32W$VQ`>D3J`DQpA&!B5`gzC`dsXSLbe8+x8;+DZK
zlftq?$QKRM(Z)u9WXDHQD8qDhqW^_Xhrp(%xsFb4&E8Yr;tUtz_4ThG9&Nf?yuK}O
z|LZKDUPl$!Fw%m8jOn&dlvL`p;hdON7A=ChCkKW6%58@9g2xFZ!TX|Z`*^9I>1wzT}&}lzgDyTf~Wc%)e6d5u@0%q
zW=$VX;!b?@kBI0kjH8`TE#vLxpxgKnu8fUY#m5(QUr?iA+WC065_A97o?g`}OEdIKZ-$*8mAIEm@M(iB%wp65|F+X~EPkCNhRkh|J&Z90m
zjHn0F+?|Q>PwmJTgX-%=_|6>Ai+J(q^pyH=jvhJj!aLKc$lhujd%rI`^XjgQh65h^
zKi+uvwf;;H(Uwm%zJ$4d=||;^P5PAcxy+bskh4?=_J}-PwR)DC$a6|+lk0d^CbiyE
zY$9r*SF7j@JnSs=9oy!JvKAcv8oY*0k*jd#k_CK<=`!;EB&VL?qL3r!gu6a%U;BFw
zNg-Rw9~tP~1*HManS8$aHUH+9oM1V}=G~T-?ER76_$I*iCu(weFR!JZ{1v0PT6>4m
z*zS2c8y{Ga_qO$jN=@MaCAVzy-nS_iFAHtlJ@SP2XZNQ1^;{L(^Msvefl5!ga3_Ka
zdCl9~izhenS_yOQVTG@?7fuC_rCYe$1XA%Han={6`1_*wA=1G+-#I_?V)rCBn&!Nk
z>P&w~YFj#iu43+=_<_uHkVc;<+YDsABsJEH6{3s$Tpiy%$&q`>!8b>P*)qm(?
zXP*bm=RFCfBNTLRNnJ+==O4ljdM6$+ORCzA6Hq|VhM70EZn7uuL&U%AO|-1M{Dz2w
zQ3uHQ{?_sws1)jZ#lQxlvS0^MO`nBQwO+zdg~h_Cf70Pj{KJBveJ4P}bRRf$RQ^EF
z{|Edp>Oj11)lL%CeZyLh}7I?mH&s~FaYa)n`v6$lN
z5m3QwKX1&ExNL9F_VjttrKBn&_n{vHcPIsApD)Fab|u^KyRiHmLGo-ck(DhvvJC~E
zjo^d7q*;zJAWu@>V}F<~x!$}-LBz~c1gV-8(>~Z*|BznN1A#%T=bYH8m>OSN2ByI{#9rEb)Ez7mMId(fDqu
zquP(OCW$%SxeDu6HK;WIx@4|$W}WOg%GtZg^VeUHe7(Hzk*C3oxS)3pHA@d}MB#|w
z#&0HR&(vJJ&%ge4c8W?$njxXAnfB%o!D
zP0lhf)E1l+tnua%K@jizx+$nYPm-8oW|wb8{i+$KeBi0I4rL8S5eUW@x*O+a`1L|c
zH5S)z-RdlJi;f~ZAeW5KC&4=6VHz;I@q*LVpFWP)_L_lt1v2g(tlp+8)6+raHaI(5
z&jhi{O$u_9@34XBePknb2fC2hF5d+f5}_<6Mt=5`^(Tph5z87{jneWf$hacC^n-hr
z)xDy)C#-)uG~IvCf7$7udM4g3j{w=C%r{>HbKb_HHs4Ow5x4Cc8#Z5!JdrLo4m~@q
z7EdFqOVdL=iC@)(MobUcj8+M(ceGWLtZUKLW+vli2AI?Xdy2M?zmmse)hbN%hz6s~v)TCiu`rhBc>
z*X9NP7jj{v{m1Z*Phsdp*T#SnxtI3YhJy%{PoEhn0qL#Kbkl@fdyKlkk!Wz`@P+Bs
z;X*Sd%8e#%dON|{Q%Fru636&fmSh2ryE)D6m`0x@&m#=2Z2AT=nZJA{lx540TW({%
zM@p|jLjw9s6nc8h`nLoPxG!u}nNa(32!@`qnCT)yr}Y1AUp9Rq={t;Nd)`3aF8SnN
zBMeS37Ojlf2noKwtj2QgPn5Jy-rbTVDJG2_zkj1at}!+tt|Mxrsw8N7LN$`J_oIuK
zdFDT;#W(F8u*v<)O(qy0c+Y25B{SBMfR;xQ$>rIg4B2BNv@8K6PDE2E9rMJ<)t8o`
z$-*F2c#nV~*6!6(UxxEBAY7#;-4PiEIW;_cxs(HhppI*m(|gODY=3;J1Qb`OI|}
z&XI-7@8uNuo_6RZ!0y6-Z&{U54rp*e?;C1b7?7)U7A=RmOUN{Q$dWmk4KzuHl66fd
z1$i>C;Q?!kDeJn$<*EO&X)12;I;mK3vY}LI^Xr{#>;WAg@zXYpgiH_)>r3-JJ(sjr
z{T5<~5yVt1Q^*lbenjTNyg^pqepdf36H(`1I#8J$7mEgsr-s-B7%g-dfM(W{Vzq
zjgY=p?_fY&A2S(@b&HHPhZEMr;l0N`ucW^grFqVo$$$f)`c3O0`2g!1d>J$QFHy+Y
z`?JZLFc$cVD>ksY*BMLT1pW#E9t}ls3+X%<33Wd;weJ{+M=Rm8d`~l4-}^%{3ufFI
zN>H%s>P8;B7RX@PAIiL?;KRSJ)-RKj*rx1r3`8T#|H0RRoAT}NwX%SSBnQ@p$?xly
zcVhp#O*qTn|ET>c52``DXXDQsK>e-s>iF5foJ61r;Gnr3sOXY@mC%R@lA0NOj``7iL
zfP#@sJ&4T$MLEAy0WsaV*$UWFLA7Ns>@K$6uN@*QT=+3CIyEw#j(W_Y%)mdz7Q@N~#5sA;6
z?*nHC%YQE{tZ-v}%f}A9^jYLHZEcMxW1^o^Fg$WcjO`ZYrc1c7?E%$&g7?^#g=<#AvH<#%Vp_&k&8R|V
zYW-098v;BlUO7Hu9KIO5tVmLEST*&1T{OKwXnmgwdx8q|Dltv@>$x`jI4aoZXz?#z
zVeY&9e8ysfFjX5Hk+IsAC2_tJ_xS?f9mF^iMt;lknE+epB1LZRK~LqYaa1Re=CA9S
z;Mg1IKXe%hgT8d0$j#S2lo4Ntp+BuZQFHlxF`SCQQf8|*E^CF;l}lP3S6Wi4+@|xQ
zn@#_*nC@8ij0AKMxsG>-oQuk5rlyft46dRC^OQ%WFW&V>7VFvaP}7JGE53i4$u^9-
zcv6E2Iok?4d*OK~t#T$EIPkX?+vA&yp&I?VP9|S29=$WrM<@Q~$jk%Yd_D1?MwMQy
zh8Qy;n^CQ|tM?wMcd_X+UKXfz9jJu!WQe_t<{HH+&wTa%Zc3aod}oJ#US~FJ9CdCt
z;E6D(eoc(DIw@hx+apF*RVXYN_^yj=={S^G0Y#+!>K+-n@}73*>{hOimCp6$iqeDI
zEY>Cjvm*2=`CL|IN=M*2RTA^$6`A@wMiJkAD=p}Hjl~edIiJeZhaJ)_G8jEwN{NgX
zNpcsS!>PZJ0;OwjIR^WpQ^#AKBEG+oFe+^)Q1`Bg3YBNI)C2EMsmzQmj4BOai2ne~
C@Hk`u
diff --git a/app/resources/data/icons/phone.png b/app/resources/data/icons/phone.png
deleted file mode 100644
index 9b32eb06767b6c537bdb34b18e3cd92b78fc7fb7..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 12454
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