diff --git a/app/DataBase/msg.py b/app/DataBase/msg.py
index 70b3b99..d1a7c44 100644
--- a/app/DataBase/msg.py
+++ b/app/DataBase/msg.py
@@ -4,6 +4,7 @@ import sqlite3
import threading
import traceback
+from app.DataBase.hard_link import parseBytes
from app.log import logger
from app.util.compress_content import parser_reply
from app.util.protocbuf.msg_pb2 import MessageBytesExtra
@@ -629,4 +630,33 @@ if __name__ == '__main__':
msg.init_database()
wxid = 'wxid_0o18ef858vnu22'
wxid = '24521163022@chatroom'
- print(msg.get_messages(wxid)[0])
+ 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:
+ xml = compress_content.decompress_CompressContent(msg[11])
+ root = ET.XML(xml)
+ appmsg = root.find('appmsg')
+ title = appmsg.find('title').text
+ des = appmsg.find('des').text
+ url = appmsg.find('url').text
+ appinfo = root.find('appinfo')
+ show_display_name = appmsg.find('sourcedisplayname')
+ if show_display_name is not None:
+ show_display_name = show_display_name.text
+ else:
+ show_display_name = appinfo.find('appname').text
+ print(title, des, url, show_display_name)
+ bytesDict = parseBytes(msg[10])
+ for msginfo in bytesDict[3]:
+ print(msginfo)
+ if msginfo[1][1][1] == 3:
+ thumb = msginfo[1][2][1]
+ print(thumb)
+ if msginfo[1][1][1] == 4:
+ app_logo = msginfo[1][2][1]
+ print('logo',app_logo)
\ No newline at end of file
diff --git a/app/DataBase/output_pc.py b/app/DataBase/output_pc.py
index 41a8bc8..87f8d0c 100644
--- a/app/DataBase/output_pc.py
+++ b/app/DataBase/output_pc.py
@@ -20,7 +20,7 @@ from ..DataBase import media_msg_db, hard_link_db, micro_msg_db, msg_db
from ..log import logger
from ..person import Me
from ..util import path
-from ..util.compress_content import parser_reply, music_share
+from ..util.compress_content import parser_reply, music_share, share_card
from ..util.emoji import get_emoji_url
from ..util.file import get_file
from ..util.music import get_music_path
@@ -123,12 +123,14 @@ class Output(QThread):
@return:
"""
return
+
def output_emoji(self):
"""
导出全部表情包
@return:
"""
return
+
def to_csv_all(self):
"""
导出全部聊天记录到CSV
@@ -658,6 +660,41 @@ class ChildThread(QThread):
artist:'{content.get('artist')}', website_name:'{content.get('website_name')}'}},'''
)
+ def share_card(self, doc, message):
+ origin_docx_path = f"{os.path.abspath('.')}/data/聊天记录/{self.contact.remark}"
+ is_send = message[4]
+ timestamp = message[5]
+ bytesExtra = message[10]
+ compress_content_ = message[11]
+ card_data = share_card(bytesExtra, compress_content_)
+ is_chatroom = 1 if self.contact.is_chatroom else 0
+ avatar = self.get_avatar_path(is_send, message)
+ display_name = self.get_display_name(is_send, message)
+ thumbnail = ''
+ if card_data.get('thumbnail'):
+ thumbnail = os.path.join(Me().wx_dir, card_data.get('thumbnail'))
+ if os.path.exists(thumbnail):
+ shutil.copy(thumbnail, os.path.join(origin_docx_path, 'image', os.path.basename(thumbnail)))
+ thumbnail = './image/' + os.path.basename(thumbnail)
+ else:
+ thumbnail = ''
+ app_logo = ''
+ if card_data.get('app_logo'):
+ app_logo = os.path.join(Me().wx_dir, card_data.get('app_logo'))
+ if os.path.exists(app_logo):
+ shutil.copy(app_logo, os.path.join(origin_docx_path, 'image', os.path.basename(app_logo)))
+ app_logo = './image/' + os.path.basename(app_logo)
+ else:
+ app_logo = ''
+ if self.output_type == Output.HTML:
+ doc.write(
+ f'''{{ type:49,sub_type:5, text:'',is_send:{is_send},avatar_path:'{avatar}',url:'{card_data.get('url')}',
+ timestamp:{timestamp},is_chatroom:{is_chatroom},displayname:'{display_name}',title:'{card_data.get('title')}',
+ description:'{card_data.get('description')}',thumbnail:'{thumbnail}',app_logo:'{app_logo}',
+ app_name:'{card_data.get('app_name')}'
+ }},\n'''
+ )
+
def to_csv(self):
origin_docx_path = f"{os.path.abspath('.')}/data/聊天记录/{self.contact.remark}"
os.makedirs(origin_docx_path, exist_ok=True)
@@ -732,6 +769,8 @@ class ChildThread(QThread):
self.file(f, message)
elif type_ == 49 and sub_type == 3 and self.message_types.get(4903):
self.music_share(f, message)
+ elif type_ == 49 and sub_type == 5 and self.message_types.get(4905):
+ self.share_card(f, message)
f.write(html_end)
f.close()
self.okSignal.emit(1)
diff --git a/app/resources/data/pause.png b/app/resources/data/pause.png
new file mode 100644
index 0000000..cdf0cf4
Binary files /dev/null and b/app/resources/data/pause.png differ
diff --git a/app/resources/data/play.png b/app/resources/data/play.png
new file mode 100644
index 0000000..09f6fe2
Binary files /dev/null and b/app/resources/data/play.png differ
diff --git a/app/resources/data/template.html b/app/resources/data/template.html
index e311144..0abcf6a 100644
--- a/app/resources/data/template.html
+++ b/app/resources/data/template.html
@@ -390,6 +390,57 @@ input {
color: #555;
font-size: 14px;
}
+
+/* 分享的卡片 */
+.card {
+ background-color: #fff;
+ border-radius: 10px;
+ overflow: hidden;
+ box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
+ max-width: 400px;
+ width: 400px;
+ display: flex;
+ flex-direction: column;
+ text-align: left;
+ margin: 20px;
+}
+.card a {
+ text-decoration: none; /* 去掉链接的下划线 */
+ color: inherit; /* 继承父元素的文字颜色 */
+
+}
+.card-content {
+ padding: 20px;
+ flex: 1;
+}
+
+.thumbnail {
+ width: 50px;
+ height: 50px;
+ object-fit: cover;
+}
+.description{
+ display: flex;
+ justify-content: space-between;
+}
+.link-info {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: 10px;
+ background-color: #f0f0f0;
+}
+
+.app-logo {
+ width: 30px;
+ height: 30px;
+ margin-right: 10px;
+ border-radius: 50%;
+}
+
+.app-name {
+ font-weight: bold;
+}
@media screen and (max-width: 768px) {
body{
display: flex; /* 使用 Flex 布局 */
@@ -498,7 +549,7 @@ input {
}
}
- const itemsPerPage = 100; // 每页显示的元素个数
+ const itemsPerPage = 1000; // 每页显示的元素个数
let currentPage = 1; // 当前页
var reachedBottom = false; // 到达底部的标记
var lastScrollTop = 0;
@@ -638,6 +689,7 @@ input {
}
return text;
}
+
// 生成各类标签的函数
function messageBubble(message, side) {
const messageBubbleTag = document.createElement('div');
@@ -687,6 +739,12 @@ input {
messageAudioTag.innerHTML = ``;
return messageAudioTag;
}
+ function messageCard(message) {
+ const messageTag = document.createElement('div');
+ messageTag.className = `card`;
+ messageTag.innerHTML = `${message.title}
${message.description}

${message.app_name} `;
+ return messageTag;
+ }
function messageFileBox(message) {
const messageFileTag = document.createElement('div');
messageFileTag.className = `chat-file`;
@@ -835,7 +893,7 @@ input {
messageElement.appendChild(message.is_send ? messageContent : avatarTag);
messageElement.appendChild(message.is_send ? avatarTag : messageContent);
}
- if (message.sub_type == 6) {
+ else if (message.sub_type == 6) {
// displayname 和 file
messageContent.className = `content-wrapper content-wrapper-${side}`;
if (message.is_chatroom && !message.is_send) {
@@ -848,7 +906,7 @@ input {
messageElement.appendChild(message.is_send ? messageContent : avatarTag);
messageElement.appendChild(message.is_send ? avatarTag : messageContent);
}
- if (message.sub_type == 3) {
+ else if (message.sub_type == 3) {
// displayname 和 file
messageContent.className = `content-wrapper content-wrapper-${side}`;
if (message.is_chatroom && !message.is_send) {
@@ -861,6 +919,19 @@ input {
messageElement.appendChild(message.is_send ? messageContent : avatarTag);
messageElement.appendChild(message.is_send ? avatarTag : messageContent);
}
+ else if(message.sub_type == 5){
+ // displayname 和 file
+ messageContent.className = `content-wrapper content-wrapper-${side}`;
+ if (message.is_chatroom && !message.is_send) {
+ messageContent.appendChild(displayNameBox(message));
+ }
+ messageContent.appendChild(messageCard(message));
+
+ // 整合
+ messageElement.className = `item item-${side}`;
+ messageElement.appendChild(message.is_send ? messageContent : avatarTag);
+ messageElement.appendChild(message.is_send ? avatarTag : messageContent);
+ }
}
else if (message.type == 34) {
diff --git a/app/ui/contact/export_dialog.py b/app/ui/contact/export_dialog.py
index 2a89f53..1bd92f9 100644
--- a/app/ui/contact/export_dialog.py
+++ b/app/ui/contact/export_dialog.py
@@ -14,6 +14,7 @@ types = {
'表情包': 47,
'音乐与音频': 4903,
'文件': 4906,
+ '分享卡片':4905,
'拍一拍等系统消息': 10000
}
Stylesheet = """
@@ -34,7 +35,7 @@ class ExportDialog(QDialog):
if file_type == 'html':
self.export_type = Output.HTML
self.export_choices = {"文本": True, "图片": True, "语音": False, "视频": False, "表情包": False,
- '音乐与音频': False,'文件': False,
+ '音乐与音频': False,'分享卡片':False,'文件': False,
'拍一拍等系统消息': True} # 定义导出的数据类型,默认全部选择
elif file_type == 'csv':
self.export_type = Output.CSV
diff --git a/app/util/compress_content.py b/app/util/compress_content.py
index 8450e81..245018b 100644
--- a/app/util/compress_content.py
+++ b/app/util/compress_content.py
@@ -7,6 +7,7 @@ import requests
from urllib.parse import urlparse
from bs4 import BeautifulSoup
+from app.DataBase.hard_link import parseBytes
def decompress_CompressContent(data):
@@ -20,13 +21,15 @@ def decompress_CompressContent(data):
try:
dst = lz4.block.decompress(data, uncompressed_size=len(data) << 10)
decoded_string = dst.decode().replace('\x00', '') # Remove any null characters
- except :
+ except:
print("Decompression failed: potentially corrupt input or insufficient buffer size.")
return ''
return decoded_string
def escape_js_and_html(input_str):
+ if not input_str:
+ return ''
# 转义HTML特殊字符
html_escaped = html.escape(input_str, quote=False)
@@ -125,6 +128,40 @@ def music_share(data: bytes):
}
+def share_card(bytesExtra, compress_content_):
+ xml = decompress_CompressContent(compress_content_)
+ root = ET.XML(xml)
+ appmsg = root.find('appmsg')
+ title = appmsg.find('title').text
+ des = appmsg.find('des').text
+ url = appmsg.find('url').text
+ appinfo = root.find('appinfo')
+ show_display_name = appmsg.find('sourcedisplayname')
+ if show_display_name is not None:
+ show_display_name = show_display_name.text
+ else:
+ if appinfo is not None:
+ show_display_name = appinfo.find('appname').text
+ bytesDict = parseBytes(bytesExtra)
+ app_logo = ''
+ thumbnail = ''
+ for msginfo in bytesDict[3]:
+ if msginfo[1][1][1] == 3:
+ thumbnail = msginfo[1][2][1]
+ thumbnail = "\\".join(thumbnail.split('\\')[1:])
+ if msginfo[1][1][1] == 4:
+ app_logo = msginfo[1][2][1]
+ app_logo = "\\".join(app_logo.split('\\')[1:])
+ return {
+ 'title': escape_js_and_html(title),
+ 'description': escape_js_and_html(des),
+ 'url': url,
+ 'app_name': escape_js_and_html(show_display_name),
+ 'thumbnail': thumbnail,
+ 'app_logo': app_logo
+ }
+
+
def get_website_name(url):
parsed_url = urlparse(url)
domain = f"{parsed_url.scheme}://{parsed_url.netloc}"
@@ -146,7 +183,7 @@ def get_website_name(url):
website_name = soup.title.string.strip()
index = website_name.find("-")
if index != -1: # 如果找到了 "-"
- website_name = website_name[index+1:].strip()
+ website_name = website_name[index + 1:].strip()
except Exception as e:
print(f"Get Website Info Error: {e}")
return website_name
@@ -162,7 +199,7 @@ def get_audio_url(url):
elif response.status_code == 200:
print('音乐文件已失效,url:' + url)
else:
- print('音乐文件地址获取失败,url:' + url +',状态码' + str(response.status_code))
+ print('音乐文件地址获取失败,url:' + url + ',状态码' + str(response.status_code))
except Exception as e:
print(f"Get Audio Url Error: {e}")
return path