diff --git a/app/DataBase/exporter_csv.py b/app/DataBase/exporter_csv.py index 55b2e12..2d50d34 100644 --- a/app/DataBase/exporter_csv.py +++ b/app/DataBase/exporter_csv.py @@ -14,11 +14,7 @@ class CSVExporter(ExporterBase): columns = ['localId', 'TalkerId', 'Type', 'SubType', 'IsSender', 'CreateTime', 'Status', 'StrContent', 'StrTime', 'Remark', 'NickName', 'Sender'] - if self.contact.is_chatroom: - packagemsg = PackageMsg() - messages = packagemsg.get_package_message_by_wxid(self.contact.wxid) - else: - messages = msg_db.get_messages(self.contact.wxid) + messages = msg_db.get_messages(self.contact.wxid) # 写入CSV文件 with open(filename, mode='w', newline='', encoding='utf-8-sig') as file: writer = csv.writer(file) diff --git a/app/DataBase/exporter_docx.py b/app/DataBase/exporter_docx.py index 65a9fd1..4fe6f56 100644 --- a/app/DataBase/exporter_docx.py +++ b/app/DataBase/exporter_docx.py @@ -1,27 +1,20 @@ import os import shutil -import sys import time -import traceback from re import findall import docx -from PyQt5.QtCore import pyqtSignal, QThread from docx import shared from docx.enum.table import WD_ALIGN_VERTICAL from docx.enum.text import WD_COLOR_INDEX, WD_PARAGRAPH_ALIGNMENT from docx.oxml.ns import qn -from app.DataBase import msg_db, hard_link_db, media_msg_db +from app.DataBase import msg_db, hard_link_db from app.DataBase.output import ExporterBase, escape_js_and_html from app.DataBase.package_msg import PackageMsg -from app.log import logger from app.person import Me -from app.util import path from app.util.compress_content import parser_reply, share_card, music_share -from app.util.emoji import get_emoji_url -from app.util.file import get_file -from app.util.image import get_image_path, get_image, get_image_abs_path +from app.util.image import get_image_abs_path from app.util.music import get_music_path @@ -296,11 +289,7 @@ class DocxExporter(ExporterBase): doc = docx.Document() doc.styles['Normal'].font.name = u'Cambria' doc.styles['Normal']._element.rPr.rFonts.set(qn('w:eastAsia'), u'宋体') - if self.contact.is_chatroom: - packagemsg = PackageMsg() - messages = packagemsg.get_package_message_by_wxid(self.contact.wxid) - else: - messages = msg_db.get_messages(self.contact.wxid) + messages = msg_db.get_messages(self.contact.wxid) Me().save_avatar(os.path.join(f"{origin_docx_path}/avatar/{Me().wxid}.png")) if self.contact.is_chatroom: for message in messages: diff --git a/app/DataBase/exporter_html.py b/app/DataBase/exporter_html.py index 134abcf..7ff941f 100644 --- a/app/DataBase/exporter_html.py +++ b/app/DataBase/exporter_html.py @@ -276,11 +276,7 @@ class HtmlExporter(ExporterBase): ) def export(self): - if self.contact.is_chatroom: - packagemsg = PackageMsg() - messages = packagemsg.get_package_message_by_wxid(self.contact.wxid) - else: - messages = msg_db.get_messages(self.contact.wxid) + messages = msg_db.get_messages(self.contact.wxid) filename = f"{os.path.abspath('.')}/data/聊天记录/{self.contact.remark}/{self.contact.remark}.html" file_path = './app/resources/data/template.html' if not os.path.exists(file_path): diff --git a/app/DataBase/exporter_txt.py b/app/DataBase/exporter_txt.py index d739d16..8b110f6 100644 --- a/app/DataBase/exporter_txt.py +++ b/app/DataBase/exporter_txt.py @@ -114,11 +114,7 @@ class TxtExporter(ExporterBase): origin_docx_path = f"{os.path.abspath('.')}/data/聊天记录/{self.contact.remark}" os.makedirs(origin_docx_path, exist_ok=True) filename = f"{os.path.abspath('.')}/data/聊天记录/{self.contact.remark}/{self.contact.remark}.txt" - if self.contact.is_chatroom: - packagemsg = PackageMsg() - messages = packagemsg.get_package_message_by_wxid(self.contact.wxid) - else: - messages = msg_db.get_messages(self.contact.wxid) + messages = msg_db.get_messages(self.contact.wxid) total_steps = len(messages) with open(filename, mode='w', newline='', encoding='utf-8') as f: for index, message in enumerate(messages): diff --git a/app/DataBase/micro_msg.py b/app/DataBase/micro_msg.py index 17e19cc..234a549 100644 --- a/app/DataBase/micro_msg.py +++ b/app/DataBase/micro_msg.py @@ -93,6 +93,7 @@ class MicroMsg: 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" diff --git a/app/DataBase/msg.py b/app/DataBase/msg.py index 4c67639..faa8987 100644 --- a/app/DataBase/msg.py +++ b/app/DataBase/msg.py @@ -16,6 +16,70 @@ def is_database_exist(): return os.path.exists(db_path) +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]: 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 + 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 = {} @@ -105,7 +169,7 @@ class Msg: result = self.cursor.fetchall() finally: lock.release() - return result + return parser_chatroom_message(result) if username_.__contains__('@chatroom') else result # result.sort(key=lambda x: x[5]) # return self.add_sender(result) @@ -164,7 +228,7 @@ class Msg: finally: lock.release() # result.sort(key=lambda x: x[5]) - return result + return parser_chatroom_message(result) if username_.__contains__('@chatroom') else result def get_messages_by_type(self, username_, type_, year_='all'): if not self.open_flag: @@ -629,14 +693,15 @@ if __name__ == '__main__': msg.init_database() wxid = 'wxid_0o18ef858vnu22' wxid = '24521163022@chatroom' - wxid = 'wxid_vtz9jk9ulzjt22' # si + wxid = 'wxid_vtz9jk9ulzjt22' # si print() from app.util import compress_content import xml.etree.ElementTree as ET + msgs = msg.get_messages(wxid) for msg in msgs: - if msg[2]==49 and msg[3]==5: + if msg[2] == 49 and msg[3] == 5: xml = compress_content.decompress_CompressContent(msg[11]) root = ET.XML(xml) appmsg = root.find('appmsg') @@ -658,4 +723,4 @@ if __name__ == '__main__': print(thumb) if tmp.field2 == 4: app_logo = tmp.field2 - print('logo',app_logo) \ No newline at end of file + print('logo', app_logo) diff --git a/app/DataBase/package_msg.py b/app/DataBase/package_msg.py index 3b96d71..a371d89 100644 --- a/app/DataBase/package_msg.py +++ b/app/DataBase/package_msg.py @@ -110,7 +110,6 @@ class PackageMsg: a[12]: msg_sender, (ContactPC 或 ContactDefault 类型,这个才是群聊里的信息发送人,不是群聊或者自己是发送者没有这个字段) ''' updated_messages = [] # 用于存储修改后的消息列表 - chatroom_members = self.get_chatroom_member_list(chatroom_wxid) messages = msg_db.get_messages(chatroom_wxid) for row in messages: message = list(row) @@ -150,7 +149,7 @@ class PackageMsg: contact.smallHeadImgBLOG = misc_db.get_avatar_buffer(contact.wxid) contact.set_avatar(contact.smallHeadImgBLOG) message.append(contact) - updated_messages.append(message) + updated_messages.append(tuple(message)) return updated_messages def get_chatroom_member_list(self, strtalker): diff --git a/app/components/bubble_message.py b/app/components/bubble_message.py index b66ae3e..3c90a26 100644 --- a/app/components/bubble_message.py +++ b/app/components/bubble_message.py @@ -1,11 +1,12 @@ import os.path +import subprocess +import platform -from PIL import Image 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, QScrollBar + QScrollArea from app.components.scroll_bar import ScrollBar @@ -83,7 +84,7 @@ class Notice(QLabel): def __init__(self, text, type_=3, parent=None): super().__init__(text, parent) self.type_ = type_ - self.setFont(QFont('微软雅黑', 12)) + self.setFont(QFont('微软雅黑', 10)) self.setWordWrap(True) self.setTextInteractionFlags(Qt.TextSelectableByMouse) self.setAlignment(Qt.AlignCenter) @@ -100,6 +101,19 @@ class Avatar(QLabel): 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__() @@ -107,8 +121,7 @@ class OpenImageThread(QThread): def run(self) -> None: if os.path.exists(self.image_path): - image = Image.open(self.image_path) - image.show() + open_image_viewer(self.image_path) class ImageMessage(QLabel): @@ -121,6 +134,9 @@ class ImageMessage(QLabel): 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) if isinstance(image, str): pixmap = QPixmap(image) self.image_path = image @@ -129,8 +145,7 @@ class ImageMessage(QLabel): self.set_image(pixmap) if image_link: self.image_path = image_link - self.setMaximumWidth(self.max_width) - self.setMaximumHeight(self.max_height) + if is_send: self.setAlignment(Qt.AlignCenter | Qt.AlignRight) # self.setScaledContents(True) @@ -151,7 +166,7 @@ class ImageMessage(QLabel): class BubbleMessage(QWidget): - def __init__(self, str_content, avatar, Type, is_send=False, parent=None): + def __init__(self, str_content, avatar, Type,is_send=False, display_name=None, parent=None): super().__init__(parent) self.isSend = is_send # self.set @@ -173,17 +188,30 @@ class BubbleMessage(QWidget): self.message = ImageMessage(str_content, is_send) else: raise ValueError("未知的消息类型") - + if display_name: + label_name = QLabel(display_name,self) + if is_send: + label_name.setAlignment(Qt.AlignRight) + vlayout = QVBoxLayout() + vlayout.setSpacing(0) + 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) - layout.addWidget(self.message, 1) + 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) - layout.addWidget(self.message, 1) + if display_name: + layout.addLayout(vlayout,1) + else: + layout.addWidget(self.message, 1) layout.addItem(self.spacerItem) self.setLayout(layout) @@ -206,8 +234,6 @@ class ScrollArea(QScrollArea): ) - - class ChatWidget(QWidget): def __init__(self): super().__init__() diff --git a/app/person.py b/app/person.py index 6e0a9a9..dcb13b2 100644 --- a/app/person.py +++ b/app/person.py @@ -47,8 +47,9 @@ class Person: os.makedirs('./data/avatar', exist_ok=True) save_path = os.path.join(f'data/avatar/', self.wxid + '.png') self.avatar_path = save_path - self.avatar.save(save_path) - print('保存头像', save_path) + if not os.path.exists(save_path): + self.avatar.save(save_path) + print('保存头像', save_path) @singleton diff --git a/app/ui/chat/chat_info.py b/app/ui/chat/chat_info.py index 70d1e83..f71d5c9 100644 --- a/app/ui/chat/chat_info.py +++ b/app/ui/chat/chat_info.py @@ -36,6 +36,8 @@ class ChatInfo(QWidget): self.setLayout(self.vBoxLayout) def show_chats(self): + # Me().save_avatar() + # self.contact.save_avatar() self.show_chat_thread = ShowChatThread(self.contact) self.show_chat_thread.showSingal.connect(self.add_message) self.show_chat_thread.finishSingal.connect(self.show_finish) @@ -79,6 +81,29 @@ class ChatInfo(QWidget): return True return False + def get_avatar_path(self, is_send, message, is_absolute_path=False) -> str: + if self.contact.is_chatroom: + avatar = message[12].smallHeadImgUrl + else: + avatar = Me().smallHeadImgUrl if is_send else self.contact.smallHeadImgUrl + if is_absolute_path: + if self.contact.is_chatroom: + # message[12].save_avatar() + avatar = message[12].avatar + else: + avatar = Me().avatar if is_send else self.contact.avatar + return avatar + + def get_display_name(self, is_send, message) -> str: + if self.contact.is_chatroom: + if is_send: + display_name = Me().name + else: + display_name = message[12].remark + else: + display_name = None + return display_name + def add_message(self, message): try: type_ = message[2] @@ -86,7 +111,8 @@ class ChatInfo(QWidget): str_time = message[8] # print(type_, type(type_)) is_send = message[4] - avatar = Me().avatar if is_send else self.contact.avatar + avatar = self.get_avatar_path(is_send, message,True) + display_name = self.get_display_name(is_send, message) timestamp = message[5] BytesExtra = message[10] if type_ == 1: @@ -98,7 +124,8 @@ class ChatInfo(QWidget): str_content, avatar, type_, - is_send + is_send, + display_name=display_name ) self.chat_window.add_message_item(bubble_message, 0) elif type_ == 3: @@ -107,7 +134,7 @@ class ChatInfo(QWidget): time_message = Notice(self.last_str_time) self.last_str_time = str_time self.chat_window.add_message_item(time_message, 0) - image_path = hard_link_db.get_image(content=str_content,bytesExtra=BytesExtra, thumb=False) + image_path = hard_link_db.get_image(content=str_content, bytesExtra=BytesExtra, thumb=False) image_path = get_abs_path(image_path) bubble_message = BubbleMessage( image_path, @@ -132,7 +159,7 @@ class ChatInfo(QWidget): self.chat_window.add_message_item(bubble_message, 0) elif type_ == 10000: str_content = str_content.lstrip('').rstrip('') - message = Notice(str_content ) + message = Notice(str_content) self.chat_window.add_message_item(message, 0) except: print(message) diff --git a/app/ui/mainview.py b/app/ui/mainview.py index e719283..e084dec 100644 --- a/app/ui/mainview.py +++ b/app/ui/mainview.py @@ -79,10 +79,7 @@ class MainWinController(QMainWindow, mainwindow.Ui_MainWindow,QCursorGif): self.outputThread0 = None self.outputThread = None self.setupUi(self) - # 设置忙碌光标图片数组 - self.initCursor([':/icons/icons/Cursors/%d.png' % - i for i in range(8)]) - self.setCursorTimeout(100) + # self.setWindowIcon(Icon.MainWindow_Icon) pixmap = QPixmap(Icon.logo_ico_path) icon = QIcon(pixmap) @@ -97,6 +94,14 @@ class MainWinController(QMainWindow, mainwindow.Ui_MainWindow,QCursorGif): self.label = QLabel(self) self.label.setGeometry((self.width() - 300) // 2, (self.height() - 100) // 2, 300, 100) self.label.setPixmap(QPixmap(':/icons/icons/loading.svg')) + self.menu_output.setIcon(Icon.Output) + self.action_output_CSV.setIcon(Icon.ToCSV) + self.action_output_CSV.triggered.connect(self.output) + self.action_output_contacts.setIcon(Icon.Output) + self.action_output_contacts.triggered.connect(self.output) + self.action_batch_export.setIcon(Icon.Output) + self.action_batch_export.triggered.connect(self.output) + self.action_desc.setIcon(Icon.Help_Icon) def load_data(self, flag=True): if os.path.exists('./app/data/info.json'): @@ -119,15 +124,15 @@ class MainWinController(QMainWindow, mainwindow.Ui_MainWindow,QCursorGif): ) def init_ui(self): + + # 设置忙碌光标图片数组 + self.initCursor([':/icons/icons/Cursors/%d.png' % + i for i in range(8)]) + self.setCursorTimeout(100) + self.startBusy() - self.menu_output.setIcon(Icon.Output) - self.action_output_CSV.setIcon(Icon.ToCSV) - self.action_output_CSV.triggered.connect(self.output) - self.action_output_contacts.setIcon(Icon.Output) - self.action_output_contacts.triggered.connect(self.output) - self.action_batch_export.setIcon(Icon.Output) - self.action_batch_export.triggered.connect(self.output) - self.action_desc.setIcon(Icon.Help_Icon) + + self.action_help_contact.triggered.connect( lambda: QDesktopServices.openUrl(QUrl("https://blog.lc044.love/post/5"))) self.action_help_chat.triggered.connect( diff --git a/app/ui/tool/pc_decrypt/pc_decrypt.py b/app/ui/tool/pc_decrypt/pc_decrypt.py index 8848b6b..7a66d1a 100644 --- a/app/ui/tool/pc_decrypt/pc_decrypt.py +++ b/app/ui/tool/pc_decrypt/pc_decrypt.py @@ -130,7 +130,7 @@ class DecryptControl(QWidget, decryptUi.Ui_Dialog, QCursorGif): return if self.info.get('key') == 'None': QMessageBox.critical(self, "错误", - "密钥错误\n将软件放在桌面上试试\n如果还不可以的话我也我能为力,您可以等待后续版本解决该问题") + "密钥错误\n请查看教程解决相关问题") close_db() self.thread2 = DecryptThread(db_dir, self.info['key']) self.thread2.maxNumSignal.connect(self.setProgressBarMaxNum) @@ -232,11 +232,13 @@ class DecryptThread(QThread): try: shutil.copy2("app/DataBase/Msg/MSG0.db", target_database) # 使用一个数据库文件作为模板 except FileNotFoundError: + logger.error(traceback.format_exc()) self.errorSignal.emit(True) # 合并数据库 try: merge_databases(source_databases, target_database) except FileNotFoundError: + logger.error(traceback.format_exc()) QMessageBox.critical("错误", "数据库不存在\n请检查微信版本是否为最新") # 音频数据库文件 @@ -248,11 +250,13 @@ class DecryptThread(QThread): try: shutil.copy2("app/DataBase/Msg/MediaMSG0.db", target_database) # 使用一个数据库文件作为模板 except FileNotFoundError: + logger.error(traceback.format_exc()) self.errorSignal.emit(True) # 合并数据库 try: merge_MediaMSG_databases(source_databases, target_database) except FileNotFoundError: + logger.error(traceback.format_exc()) QMessageBox.critical("错误", "数据库不存在\n请检查微信版本是否为最新") self.okSignal.emit('ok') # self.signal.emit('100') diff --git a/main.py b/main.py index a19e6a9..bdb28cb 100644 --- a/main.py +++ b/main.py @@ -3,19 +3,6 @@ import sys import time import traceback -from PyQt5.QtGui import QFont -from PyQt5.QtWidgets import * -from PyQt5.QtCore import Qt - -from app.DataBase import close_db -from app.log import logger -from app.ui import mainview -from app.ui.tool.pc_decrypt import pc_decrypt -from app.config import version - -ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("WeChatReport") -QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) -QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) widget = None @@ -35,6 +22,19 @@ def excepthook(exc_type, exc_value, traceback_): # 设置 excepthook sys.excepthook = excepthook +from PyQt5.QtGui import QFont +from PyQt5.QtWidgets import * +from PyQt5.QtCore import Qt + +from app.DataBase import close_db +from app.log import logger +from app.ui import mainview +from app.ui.tool.pc_decrypt import pc_decrypt +from app.config import version + +ctypes.windll.shell32.SetCurrentProcessExplicitAppUserModelID("WeChatReport") +QApplication.setAttribute(Qt.AA_EnableHighDpiScaling, True) +QApplication.setAttribute(Qt.AA_UseHighDpiPixmaps, True) class ViewController(QWidget): diff --git a/readme.md b/readme.md index ccb26bc..bbe4cc8 100644 --- a/readme.md +++ b/readme.md @@ -272,6 +272,7 @@ python main.py - [STDquantum](https://github.com/STDquantum) - [xuanli](https://github.com/xuanli) +- [无名路人](https://github.com/wumingluren) 如果您提供赞助并希望出现在赞助者名单中,请在提交赞助时提供您的 GitHub 用户名或其他相关信息。 diff --git a/requirements.txt b/requirements.txt index 00a2106..90cc6ca 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,7 +7,6 @@ silk-python pyaudio fuzzywuzzy python-Levenshtein -Pillow==10.1.0 requests flask==3.0.0 pyecharts==2.0.1