Merge pull request #153 from STDquantum/master

html群聊导出支持头像和昵称;修复了一些其他问题
This commit is contained in:
SiYuan 2023-12-17 20:23:57 +08:00 committed by GitHub
commit 99fa715e10
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 495 additions and 107 deletions

View File

@ -57,6 +57,21 @@ class Msg:
lock.release() lock.release()
def get_messages(self, username_): def get_messages(self, username_):
'''
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,
'''
if not self.open_flag: if not self.open_flag:
return None return None
sql = ''' sql = '''

View File

@ -4,6 +4,7 @@ import os
from re import findall from re import findall
from PyQt5.QtCore import pyqtSignal, QThread from PyQt5.QtCore import pyqtSignal, QThread
from PyQt5.QtWidgets import QFileDialog from PyQt5.QtWidgets import QFileDialog
from eyed3 import load
from . import msg_db, micro_msg_db from . import msg_db, micro_msg_db
from .package_msg import PackageMsg from .package_msg import PackageMsg
@ -26,6 +27,7 @@ def makedirs(path):
os.makedirs(os.path.join(path, 'video'), exist_ok=True) os.makedirs(os.path.join(path, 'video'), exist_ok=True)
os.makedirs(os.path.join(path, 'voice'), exist_ok=True) os.makedirs(os.path.join(path, 'voice'), exist_ok=True)
os.makedirs(os.path.join(path, 'file'), exist_ok=True) os.makedirs(os.path.join(path, 'file'), exist_ok=True)
os.makedirs(os.path.join(path, 'avatar'), exist_ok=True)
def escape_js_and_html(input_str): def escape_js_and_html(input_str):
@ -136,6 +138,18 @@ class Output(QThread):
self.requestInterruption() self.requestInterruption()
def modify_audio_metadata(audiofile, new_artist): # 修改音频元数据中的“创作者”标签
audiofile = load(audiofile)
# 检查文件是否有标签
if audiofile.tag is None:
audiofile.initTag()
# 修改艺术家名称
audiofile.tag.artist = new_artist
audiofile.tag.save()
class ChildThread(QThread): class ChildThread(QThread):
""" """
子线程用于导出部分聊天记录 子线程用于导出部分聊天记录
@ -168,20 +182,32 @@ class ChildThread(QThread):
str_content = message[7] str_content = message[7]
str_time = message[8] str_time = message[8]
is_send = message[4] is_send = message[4]
avatar = 'myhead.png' if is_send else 'tahead.png'
timestamp = message[5] timestamp = message[5]
is_chatroom = 1 if self.contact.is_chatroom else 0
if is_chatroom:
avatar = f"./avatar/{message[12].wxid}.png"
else:
avatar = f"./avatar/{MePC().wxid if is_send else self.contact.wxid}.png"
if is_chatroom:
if is_send:
displayname = MePC().name
else:
displayname = message[12].remark
else:
displayname = MePC().name if is_send else self.contact.remark
displayname = escape_js_and_html(displayname)
if self.output_type == Output.HTML: if self.output_type == Output.HTML:
str_content = escape_js_and_html(str_content) str_content = escape_js_and_html(str_content)
if self.is_5_min(timestamp): if self.is_5_min(timestamp):
doc.write( doc.write(
f'''{{ type:0, text: '{str_time}',is_send:0,avatar_path:''}},''' f'''{{ type:0, text: '{str_time}',is_send:0,avatar_path:'',timestamp:{timestamp},is_chatroom:{is_chatroom},displayname:'{displayname}'}},'''
) )
emojiText = findall(r"(\[.+?\])", str_content) emojiText = findall(r"(\[.+?\])", str_content)
for emoji_text in emojiText: for emoji_text in emojiText:
if emoji_text in emoji: if emoji_text in emoji:
str_content = str_content.replace(emoji_text, emoji[emoji_text]) str_content = str_content.replace(emoji_text, emoji[emoji_text])
doc.write( doc.write(
f'''{{ type:{1}, text: '{str_content}',is_send:{is_send},avatar_path:'{avatar}'}},''' f'''{{ type:{1}, text: '{str_content}',is_send:{is_send},avatar_path:'{avatar}',timestamp:{timestamp},is_chatroom:{is_chatroom},displayname:'{displayname}'}},'''
) )
elif self.output_type == Output.TXT: elif self.output_type == Output.TXT:
name = '' if is_send else self.contact.remark name = '' if is_send else self.contact.remark
@ -195,9 +221,21 @@ class ChildThread(QThread):
str_content = message[7] str_content = message[7]
str_time = message[8] str_time = message[8]
is_send = message[4] is_send = message[4]
avatar = 'myhead.png' if is_send else 'tahead.png'
timestamp = message[5]
BytesExtra = message[10] BytesExtra = message[10]
timestamp = message[5]
is_chatroom = 1 if self.contact.is_chatroom else 0
if is_chatroom:
avatar = f"./avatar/{message[12].wxid}.png"
else:
avatar = f"./avatar/{MePC().wxid if is_send else self.contact.wxid}.png"
if is_chatroom:
if is_send:
displayname = MePC().name
else:
displayname = message[12].remark
else:
displayname = MePC().name if is_send else self.contact.remark
displayname = escape_js_and_html(displayname)
if self.output_type == Output.HTML: if self.output_type == Output.HTML:
str_content = escape_js_and_html(str_content) str_content = escape_js_and_html(str_content)
image_path = hard_link_db.get_image(str_content, BytesExtra, thumb=False) image_path = hard_link_db.get_image(str_content, BytesExtra, thumb=False)
@ -218,10 +256,10 @@ class ChildThread(QThread):
# print(f"tohtml:---{image_path}") # print(f"tohtml:---{image_path}")
if self.is_5_min(timestamp): if self.is_5_min(timestamp):
doc.write( doc.write(
f'''{{ type:0, text: '{str_time}',is_send:0,avatar_path:''}},''' f'''{{ type:0, text: '{str_time}',is_send:0,avatar_path:'',timestamp:{timestamp},is_chatroom:{is_chatroom},displayname:'{displayname}'}},'''
) )
doc.write( doc.write(
f'''{{ type:{type_}, text: '{image_path}',is_send:{is_send},avatar_path:'{avatar}'}},''' f'''{{ type:{type_}, text: '{image_path}',is_send:{is_send},avatar_path:'{avatar}',timestamp:{timestamp},is_chatroom:{is_chatroom},displayname:'{displayname}'}},'''
) )
elif self.output_type == Output.TXT: elif self.output_type == Output.TXT:
name = '' if is_send else self.contact.remark name = '' if is_send else self.contact.remark
@ -234,24 +272,37 @@ class ChildThread(QThread):
str_content = message[7] str_content = message[7]
str_time = message[8] str_time = message[8]
is_send = message[4] is_send = message[4]
avatar = 'myhead.png' if is_send else 'tahead.png'
timestamp = message[5]
msgSvrId = message[9] msgSvrId = message[9]
timestamp = message[5]
is_chatroom = 1 if self.contact.is_chatroom else 0
if is_chatroom:
avatar = f"./avatar/{message[12].wxid}.png"
else:
avatar = f"./avatar/{MePC().wxid if is_send else self.contact.wxid}.png"
if is_chatroom:
if is_send:
displayname = MePC().name
else:
displayname = message[12].remark
else:
displayname = MePC().name if is_send else self.contact.remark
displayname = escape_js_and_html(displayname)
if self.output_type == Output.HTML: if self.output_type == Output.HTML:
try: try:
audio_path = media_msg_db.get_audio(msgSvrId, output_path=origin_docx_path + "/voice") audio_path = media_msg_db.get_audio(msgSvrId, output_path=origin_docx_path + "/voice")
audio_path = audio_path.replace('/', '\\') audio_path = audio_path.replace('/', '\\')
modify_audio_metadata(audio_path, displayname)
os.utime(audio_path, (timestamp, timestamp)) os.utime(audio_path, (timestamp, timestamp))
audio_path = audio_path.replace('\\', '/') audio_path = audio_path.replace('\\', '/')
voice_to_text = media_msg_db.get_audio_text(str_content) voice_to_text = escape_js_and_html(media_msg_db.get_audio_text(str_content))
except: except:
return return
if self.is_5_min(timestamp): if self.is_5_min(timestamp):
doc.write( doc.write(
f'''{{ type:0, text: '{str_time}',is_send:0,avatar_path:''}},''' f'''{{ type:0, text: '{str_time}',is_send:0,avatar_path:'',timestamp:{timestamp},is_chatroom:{is_chatroom},displayname:'{displayname}'}},'''
) )
doc.write( doc.write(
f'''{{ type:34, text:'{audio_path}',is_send:{is_send},avatar_path:'{avatar}',voice_to_text:'{voice_to_text}'}},''' f'''{{ type:34, text:'{audio_path}',is_send:{is_send},avatar_path:'{avatar}',voice_to_text:'{voice_to_text}',timestamp:{timestamp},is_chatroom:{is_chatroom},displayname:'{displayname}'}},'''
) )
if self.output_type == Output.TXT: if self.output_type == Output.TXT:
name = '' if is_send else self.contact.remark name = '' if is_send else self.contact.remark
@ -264,17 +315,33 @@ class ChildThread(QThread):
str_content = message[7] str_content = message[7]
str_time = message[8] str_time = message[8]
is_send = message[4] is_send = message[4]
avatar = 'myhead.png' if is_send else 'tahead.png'
timestamp = message[5] timestamp = message[5]
is_chatroom = 1 if self.contact.is_chatroom else 0
if is_chatroom:
avatar = f"./avatar/{message[12].wxid}.png"
else:
avatar = f"./avatar/{MePC().wxid if is_send else self.contact.wxid}.png"
if is_chatroom:
if is_send:
displayname = MePC().name
else:
displayname = message[12].remark
else:
displayname = MePC().name if is_send else self.contact.remark
displayname = escape_js_and_html(displayname)
if self.output_type == Output.HTML: if self.output_type == Output.HTML:
emoji_path = get_emoji(str_content, thumb=True, output_path=origin_docx_path + '/emoji') emoji_path = get_emoji(str_content, thumb=True, output_path=origin_docx_path + '/emoji')
emoji_path = './emoji/' + os.path.basename(emoji_path) if emoji_path == "":
shutil.copy(f"{os.path.abspath('.')}/app/resources/icons/404.png", origin_docx_path + '/emoji/404.png')
emoji_path = "./emoji/404.png"
else:
emoji_path = './emoji/' + os.path.basename(emoji_path)
if self.is_5_min(timestamp): if self.is_5_min(timestamp):
doc.write( doc.write(
f'''{{ type:0, text: '{str_time}',is_send:0,avatar_path:''}},''' f'''{{ type:0, text: '{str_time}',is_send:0,avatar_path:'',timestamp:{timestamp},is_chatroom:{is_chatroom},displayname:'{displayname}'}},'''
) )
doc.write( doc.write(
f'''{{ type:{3}, text: '{emoji_path}',is_send:{is_send},avatar_path:'{avatar}'}},''' f'''{{ type:{3}, text: '{emoji_path}',is_send:{is_send},avatar_path:'{avatar}',timestamp:{timestamp},is_chatroom:{is_chatroom},displayname:'{displayname}'}},'''
) )
elif self.output_type == Output.TXT: elif self.output_type == Output.TXT:
name = '' if is_send else self.contact.remark name = '' if is_send else self.contact.remark
@ -302,6 +369,20 @@ class ChildThread(QThread):
avatar = 'myhead.png' if is_send else 'tahead.png' avatar = 'myhead.png' if is_send else 'tahead.png'
content = parser_reply(message[11]) content = parser_reply(message[11])
refer_msg = content.get('refer') refer_msg = content.get('refer')
timestamp = message[5]
is_chatroom = 1 if self.contact.is_chatroom else 0
if is_chatroom:
avatar = f"./avatar/{message[12].wxid}.png"
else:
avatar = f"./avatar/{MePC().wxid if is_send else self.contact.wxid}.png"
if is_chatroom:
if is_send:
displayname = MePC().name
else:
displayname = message[12].remark
else:
displayname = MePC().name if is_send else self.contact.remark
displayname = escape_js_and_html(displayname)
if self.output_type == Output.HTML: if self.output_type == Output.HTML:
contentText = content.get('title') contentText = content.get('title')
emojiText = findall(r"(\[.+?\])", contentText) emojiText = findall(r"(\[.+?\])", contentText)
@ -315,11 +396,11 @@ class ChildThread(QThread):
if emoji_text in emoji: if emoji_text in emoji:
referText = referText.replace(emoji_text, emoji[emoji_text]) referText = referText.replace(emoji_text, emoji[emoji_text])
doc.write( doc.write(
f'''{{ type:49, text: '{contentText}',is_send:{is_send},sub_type:{content.get('type')},refer_text: '{referText}',avatar_path:'{avatar}'}},''' f'''{{ type:49, text: '{contentText}',is_send:{is_send},sub_type:{content.get('type')},refer_text: '{referText}',avatar_path:'{avatar}',timestamp:{timestamp},is_chatroom:{is_chatroom},displayname:'{displayname}'}},'''
) )
else: else:
doc.write( doc.write(
f'''{{ type:49, text: '{contentText}',is_send:{is_send},sub_type:{content.get('type')},avatar_path:'{avatar}'}},''' f'''{{ type:49, text: '{contentText}',is_send:{is_send},sub_type:{content.get('type')},avatar_path:'{avatar}',timestamp:{timestamp},is_chatroom:{is_chatroom},displayname:'{displayname}'}},'''
) )
elif self.output_type == Output.TXT: elif self.output_type == Output.TXT:
name = '' if is_send else self.contact.remark name = '' if is_send else self.contact.remark
@ -336,10 +417,12 @@ class ChildThread(QThread):
str_content = message[7] str_content = message[7]
is_send = message[4] is_send = message[4]
str_time = message[8] str_time = message[8]
timestamp = message[5]
is_chatroom = 1 if self.contact.is_chatroom else 0
str_content = escape_js_and_html(str_content.lstrip('<revokemsg>').rstrip('</revokemsg>')) str_content = escape_js_and_html(str_content.lstrip('<revokemsg>').rstrip('</revokemsg>'))
if self.output_type == Output.HTML: if self.output_type == Output.HTML:
doc.write( doc.write(
f'''{{ type:0, text: '{str_content}',is_send:{is_send},avatar_path:''}},''' f'''{{ type:0, text: '{str_content}',is_send:{is_send},avatar_path:'',timestamp:{timestamp},is_chatroom:{is_chatroom},displayname:''}},'''
) )
elif self.output_type == Output.TXT: elif self.output_type == Output.TXT:
name = '' if is_send else self.contact.remark name = '' if is_send else self.contact.remark
@ -354,8 +437,20 @@ class ChildThread(QThread):
str_time = message[8] str_time = message[8]
is_send = message[4] is_send = message[4]
BytesExtra = message[10] BytesExtra = message[10]
avatar = 'myhead.png' if is_send else 'tahead.png'
timestamp = message[5] timestamp = message[5]
is_chatroom = 1 if self.contact.is_chatroom else 0
if is_chatroom:
avatar = f"./avatar/{message[12].wxid}.png"
else:
avatar = f"./avatar/{MePC().wxid if is_send else self.contact.wxid}.png"
if is_chatroom:
if is_send:
displayname = MePC().name
else:
displayname = message[12].remark
else:
displayname = MePC().name if is_send else self.contact.remark
displayname = escape_js_and_html(displayname)
if self.output_type == Output.HTML: if self.output_type == Output.HTML:
video_path = hard_link_db.get_video(str_content, BytesExtra, thumb=False) video_path = hard_link_db.get_video(str_content, BytesExtra, thumb=False)
image_path = hard_link_db.get_video(str_content, BytesExtra, thumb=True) image_path = hard_link_db.get_video(str_content, BytesExtra, thumb=True)
@ -370,14 +465,14 @@ class ChildThread(QThread):
# print(f"tohtml:---{image_path}") # print(f"tohtml:---{image_path}")
if self.is_5_min(timestamp): if self.is_5_min(timestamp):
doc.write( doc.write(
f'''{{ type:0, text: '{str_time}',is_send:0,avatar_path:''}},''' f'''{{ type:0, text: '{str_time}',is_send:0,avatar_path:'',timestamp:{timestamp},is_chatroom:{is_chatroom},displayname:'{displayname}'}},'''
) )
doc.write( doc.write(
f'''{{ type:3, text: '{image_path}',is_send:{is_send},avatar_path:'{avatar}'}},''' f'''{{ type:3, text: '{image_path}',is_send:{is_send},avatar_path:'{avatar}',timestamp:{timestamp},is_chatroom:{is_chatroom},displayname:'{displayname}'}},'''
) )
except: except:
doc.write( doc.write(
f'''{{ type:1, text: '视频丢失',is_send:{is_send},avatar_path:'{avatar}'}},''' f'''{{ type:1, text: '视频丢失',is_send:{is_send},avatar_path:'{avatar}',timestamp:{timestamp},is_chatroom:{is_chatroom},displayname:'{displayname}'}},'''
) )
return return
if video_path is None and image_path is None: if video_path is None and image_path is None:
@ -392,10 +487,10 @@ class ChildThread(QThread):
video_path = video_path.replace('\\', '/') video_path = video_path.replace('\\', '/')
if self.is_5_min(timestamp): if self.is_5_min(timestamp):
doc.write( doc.write(
f'''{{ type:0, text: '{str_time}',is_send:0,avatar_path:''}},''' f'''{{ type:0, text: '{str_time}',is_send:0,avatar_path:'',timestamp:{timestamp},is_chatroom:{is_chatroom},displayname:'{displayname}'}},'''
) )
doc.write( doc.write(
f'''{{ type:{type_}, text: '{video_path}',is_send:{is_send},avatar_path:'{avatar}'}},''' f'''{{ type:{type_}, text: '{video_path}',is_send:{is_send},avatar_path:'{avatar}',timestamp:{timestamp},is_chatroom:{is_chatroom},displayname:'{displayname}'}},'''
) )
elif self.output_type == Output.TXT: elif self.output_type == Output.TXT:
name = '' if is_send else self.contact.remark name = '' if is_send else self.contact.remark
@ -428,12 +523,27 @@ class ChildThread(QThread):
def to_html_(self): def to_html_(self):
origin_docx_path = f"{os.path.abspath('.')}/data/聊天记录/{self.contact.remark}" origin_docx_path = f"{os.path.abspath('.')}/data/聊天记录/{self.contact.remark}"
makedirs(origin_docx_path) makedirs(origin_docx_path)
messages = msg_db.get_messages(self.contact.wxid) 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)
filename = f"{os.path.abspath('.')}/data/聊天记录/{self.contact.remark}/{self.contact.remark}.html" filename = f"{os.path.abspath('.')}/data/聊天记录/{self.contact.remark}/{self.contact.remark}.html"
f = open(filename, 'w', encoding='utf-8') f = open(filename, 'w', encoding='utf-8')
f.write(html_head) f.write(html_head.replace("<title>Chat Records</title>", f"<title>{self.contact.remark}</title>"))
MePC().avatar.save(os.path.join(origin_docx_path, 'myhead.png')) MePC().avatar.save(os.path.join(f"{origin_docx_path}/avatar/{MePC().wxid}.png"))
self.contact.avatar.save(os.path.join(origin_docx_path, 'tahead.png')) if self.contact.is_chatroom:
for message in messages:
if message[4]: # is_send
continue
try:
chatroom_avatar_path = f"{origin_docx_path}/avatar/{message[12].wxid}.png"
if not os.path.exists(chatroom_avatar_path):
message[12].avatar.save(chatroom_avatar_path)
except:
pass
else:
self.contact.avatar.save(os.path.join(f"{origin_docx_path}/avatar/{self.contact.wxid}.png"))
self.rangeSignal.emit(len(messages)) self.rangeSignal.emit(len(messages))
total_steps = len(messages) total_steps = len(messages)
for index, message in enumerate(messages): for index, message in enumerate(messages):
@ -452,7 +562,7 @@ class ChildThread(QThread):
self.emoji(f, message) self.emoji(f, message)
elif type_ == 10000 and self.message_types.get(type_): elif type_ == 10000 and self.message_types.get(type_):
self.system_msg(f, message) self.system_msg(f, message)
elif type_ == 49 and sub_type == 57: elif type_ == 49 and sub_type == 57 and self.message_types.get(1):
self.refermsg(f, message) self.refermsg(f, message)
f.write(html_end) f.write(html_end)
f.close() f.close()
@ -547,6 +657,7 @@ emoji = {
'[阴险]': '<img src="https://res.wx.qq.com/t/wx_fed/we-emoji/res/v1.2.8/assets/Expression/Expression_52@2x.png" id="阴险" class="emoji_img">', '[阴险]': '<img src="https://res.wx.qq.com/t/wx_fed/we-emoji/res/v1.2.8/assets/Expression/Expression_52@2x.png" id="阴险" class="emoji_img">',
'[亲亲]': '<img src="https://res.wx.qq.com/t/wx_fed/we-emoji/res/v1.2.8/assets/Expression/Expression_53@2x.png" id="亲亲" class="emoji_img">', '[亲亲]': '<img src="https://res.wx.qq.com/t/wx_fed/we-emoji/res/v1.2.8/assets/Expression/Expression_53@2x.png" id="亲亲" class="emoji_img">',
'[可怜]': '<img src="https://res.wx.qq.com/t/wx_fed/we-emoji/res/v1.2.8/assets/Expression/Expression_55@2x.png" id="可怜" class="emoji_img">', '[可怜]': '<img src="https://res.wx.qq.com/t/wx_fed/we-emoji/res/v1.2.8/assets/Expression/Expression_55@2x.png" id="可怜" class="emoji_img">',
'[Whimper]': '<img src="https://res.wx.qq.com/t/wx_fed/we-emoji/res/v1.2.8/assets/Expression/Expression_55@2x.png" id="可怜" class="emoji_img">',
'[笑脸]': '<img src="https://res.wx.qq.com/t/wx_fed/we-emoji/res/v1.2.8/assets/newemoji/Happy.png" id="笑脸" class="emoji_img">', '[笑脸]': '<img src="https://res.wx.qq.com/t/wx_fed/we-emoji/res/v1.2.8/assets/newemoji/Happy.png" id="笑脸" class="emoji_img">',
'[生病]': '<img src="https://res.wx.qq.com/t/wx_fed/we-emoji/res/v1.2.8/assets/newemoji/Sick.png" id="生病" class="emoji_img">', '[生病]': '<img src="https://res.wx.qq.com/t/wx_fed/we-emoji/res/v1.2.8/assets/newemoji/Sick.png" id="生病" class="emoji_img">',
'[脸红]': '<img src="https://res.wx.qq.com/t/wx_fed/we-emoji/res/v1.2.8/assets/newemoji/Flushed.png" id="脸红" class="emoji_img">', '[脸红]': '<img src="https://res.wx.qq.com/t/wx_fed/we-emoji/res/v1.2.8/assets/newemoji/Flushed.png" id="脸红" class="emoji_img">',
@ -645,8 +756,10 @@ body{
display: block; display: block;
max-width: 90%; max-width: 90%;
max-height: 90%; max-height: 90%;
margin: auto; position: absolute;
margin-top: 5%; top: 50%;
left: 50%;
transform: translate(-50%, -50%);
} }
.container{ .container{
height: 99%; height: 99%;
@ -676,9 +789,10 @@ body{
word-wrap:break-word; word-wrap:break-word;
word-break:normal; word-break:normal;
} }
.chat-refer{ .chat-refer {
margin-top: 5px;
max-width: 400px; max-width: 400px;
padding: 6px; padding: 9px;
border-radius: 5px; border-radius: 5px;
position: relative; position: relative;
color: #000; color: #000;
@ -686,11 +800,11 @@ body{
word-wrap:break-word; word-wrap:break-word;
word-break:normal; word-break:normal;
} }
.chat-refer-right{ .chat-refer-right {
margin-right:55px; margin-right: 15px;
} }
.chat-refer-left{ .chat-refer-left{
margin-left:55px; margin-left: 15px;
} }
.item-left .bubble{ .item-left .bubble{
margin-left: 15px; margin-left: 15px;
@ -722,8 +836,8 @@ body{
border-bottom: 10px solid transparent; border-bottom: 10px solid transparent;
right: -20px; right: -20px;
} }
.item{ .item {
white-space: pre-line; white-space: pre-wrap;
margin-top: 15px; margin-top: 15px;
display: flex; display: flex;
width: 100%; width: 100%;
@ -750,6 +864,27 @@ body{
user-select:none; user-select:none;
} }
.content-wrapper {
display: flex;
flex-direction: column;
}
.content-wrapper-left {
align-items: baseline;
}
.content-wrapper-right {
align-items: flex-end;
}
.displayname {
margin-left: 13px;
margin-left: 13px;
font-size: 13px;
margin-bottom: 5px;
color: darkgray;
}
.chat-image img{ .chat-image img{
margin-right: 18px; margin-right: 18px;
margin-left: 18px; margin-left: 18px;
@ -767,10 +902,11 @@ body{
max-width: 350px; max-width: 350px;
} }
.chat-audio{ .chat-audio{
max-width: 300px; max-width: 400px;
} }
audio{ audio{
right: 25px; margin-left: 9px;
margin-right: 9px;
} }
.input-area{ .input-area{
border-top:0.5px solid #e0e0e0; border-top:0.5px solid #e0e0e0;
@ -874,10 +1010,10 @@ input {
} }
.emoji_img { .emoji_img {
width: 26px; width: 22px;
height: 26px; height: 22px;
position: relative; vertical-align: middle;
bottom: -6px; margin-top: -4.4px;
} }
</style> </style>
@ -945,87 +1081,146 @@ html_end = '''
const startIndex = (page - 1) * itemsPerPage; const startIndex = (page - 1) * itemsPerPage;
const endIndex = startIndex + itemsPerPage; const endIndex = startIndex + itemsPerPage;
console.log(page); console.log(page);
// 生成各类标签的函数
function messageBubble(message, side) {
const messageBubbleTag = document.createElement('div');
messageBubbleTag.className = `bubble bubble-${side}`;
messageBubbleTag.innerHTML = message.text;
return messageBubbleTag;
}
function displayNameBox(message) {
const displayName = document.createElement('div');
displayName.className = "displayname";
displayName.innerHTML = message.displayname;
return displayName;
}
function avatarBox(message) {
const avatarTag = document.createElement('div');
avatarTag.className = "avatar";
avatarTag.innerHTML = `<img src="${message.avatar_path}" />`
return avatarTag;
}
function messageImgBox(message) {
const messageImgTag = document.createElement('div');
messageImgTag.className = `chat-image`;
messageImgTag.innerHTML = `<img src="${message.text}" onclick="showModal(this)"/>`;
return messageImgTag;
}
function messageVideoBox(message) {
const messageVideoTag = document.createElement('div');
messageVideoTag.className = `chat-video`;
messageVideoTag.innerHTML = `<video src="${message.text}" controls />`;
return messageVideoTag;
}
function messageElementReferText(message, side) {
const messageElementRefer = document.createElement('div');
messageElementRefer.className = `chat-refer chat-refer-${side}`;
messageElementRefer.innerHTML = message.refer_text;
return messageElementRefer;
}
function messageVoiceToTextBubble(message, side) {
const messageVoiceToTextTag = document.createElement('div');
messageVoiceToTextTag.className = `bubble bubble-${side}`;
messageVoiceToTextTag.innerHTML = message.voice_to_text;
return messageVoiceToTextTag;
}
function messageAudioBox(message) {
const messageAudioTag = document.createElement('div');
messageAudioTag.className = `chat-audio`;
messageAudioTag.innerHTML = `<audio src="${message.text}" controls></audio>`;
return messageAudioTag;
}
// 从数据列表中取出对应范围的元素并添加到容器中 // 从数据列表中取出对应范围的元素并添加到容器中
for (let i = startIndex; i < endIndex && i < chatMessages.length; i++) { for (let i = startIndex; i < endIndex && i < chatMessages.length; i++) {
const message = chatMessages[i]; const message = chatMessages[i];
const messageElement = document.createElement('div'); const messageElement = document.createElement('div'); // 下面那俩的合体
const messageElementRefer = document.createElement('div'); const avatarTag = avatarBox(message); // 头像
const formattedText = message.text.replace(/\\n/g, "<br>"); const messageContent = document.createElement('div'); // 除了avatar之外的所有
var formattedReferText = ""; const side = message.is_send ? "right" : "left";
if (message.type == 1) { if (message.type == 1) {
if (message.is_send == 1) { // displayname bubble
messageElement.className = "item item-right"; messageContent.className = `content-wrapper content-wrapper-${side}`;
messageElement.innerHTML = `<div class='bubble bubble-right'>${formattedText}</div><div class='avatar'><img src="${message.avatar_path}" /></div>` if (message.is_chatroom && !message.is_send) {
} messageContent.appendChild(displayNameBox(message));
else if (message.is_send == 0) {
messageElement.className = "item item-left";
messageElement.innerHTML = `<div class='avatar'><img src="${message.avatar_path}" /></div><div class='bubble bubble-right'>${formattedText}</div>`
} }
messageContent.appendChild(messageBubble(message, side));
// 整合
messageElement.className = `item item-${side}`;
messageElement.appendChild(message.is_send ? messageContent : avatarTag);
messageElement.appendChild(message.is_send ? avatarTag : messageContent);
} }
else if (message.type == 0) { else if (message.type == 0) {
messageElement.className = "item item-center"; messageElement.className = "item item-center";
messageElement.innerHTML = `<span>${formattedText}</span>` messageElement.innerHTML = `<span>${message.text}</span>`
} }
else if (message.type == 3) { else if (message.type == 3) {
if (message.is_send == 1) { // displayname img
messageElement.className = "item item-right"; messageContent.className = `content-wrapper content-wrapper-${side}`;
messageElement.innerHTML = `<div class='chat-image' ><img src="${formattedText}" onclick="showModal(this)"/></div><div class='avatar'><img src="${message.avatar_path}" /></div>` if (message.is_chatroom && !message.is_send) {
} messageContent.appendChild(displayNameBox(message));
else if (message.is_send == 0) {
messageElement.className = "item item-left";
messageElement.innerHTML = `<div class='avatar'><img src="${message.avatar_path}"/></div><div class='chat-image'><img src="${formattedText}" onclick="showModal(this)"/></div>`
} }
messageContent.appendChild(messageImgBox(message));
// 整合
messageElement.className = `item item-${side}`;
messageElement.appendChild(message.is_send ? messageContent : avatarTag);
messageElement.appendChild(message.is_send ? avatarTag : messageContent);
} }
else if (message.type == 43) { else if (message.type == 43) {
if (message.is_send == 1) { // displayname video
messageElement.className = "item item-right"; messageContent.className = `content-wrapper content-wrapper-${side}`;
messageElement.innerHTML = `<div class='chat-video'><video src="${formattedText}" controls /></div><div class='avatar'><img src="${message.avatar_path}" /></div>` if (message.is_chatroom && !message.is_send) {
} messageContent.appendChild(displayNameBox(message));
else if (message.is_send == 0) {
messageElement.className = "item item-left";
messageElement.innerHTML = `<div class='avatar'><img src="${message.avatar_path}" /></div><div class='chat-video'><video src="${formattedText}" controls "/></div>`
} }
messageContent.appendChild(messageVideoBox(message));
// 整合
messageElement.className = `item item-${side}`;
messageElement.appendChild(message.is_send ? messageContent : avatarTag);
messageElement.appendChild(message.is_send ? avatarTag : messageContent);
} }
else if (message.type == 49) { else if (message.type == 49) {
if (message.sub_type == 57){ if (message.sub_type == 57) {
// displayname bubble refer
messageContent.className = `content-wrapper content-wrapper-${side}`;
if (message.is_chatroom && !message.is_send) {
messageContent.appendChild(displayNameBox(message));
}
messageContent.appendChild(messageBubble(message, side));
if (message.refer_text) { if (message.refer_text) {
formattedReferText = message.refer_text.replace(/\\n/g, "<br>"); messageContent.appendChild(messageElementReferText(message, side));
}
if (message.is_send == 1) {
messageElement.className = "item item-right";
messageElement.innerHTML = `<div class='bubble bubble-right'>${formattedText}</div><div class='avatar'><img src="${message.avatar_path}" /></div>`
if (message.refer_text) {
messageElementRefer.className = "item item-right item-refer";
messageElementRefer.innerHTML = `<div class='chat-refer chat-refer-right'>${formattedReferText}</div></div>`
}
}
else if (message.is_send == 0) {
messageElement.className = "item item-left";
messageElement.innerHTML = `<div class='avatar'><img src="${message.avatar_path}" /></div><div class='bubble bubble-left'>${formattedText}</div>`
if (message.refer_text) {
messageElementRefer.className = "item item-left item-refer";
messageElementRefer.innerHTML = `<div class='chat-refer chat-refer-left'>${formattedReferText}</div></div>`
}
} }
// 整合
messageElement.className = `item item-${side}`;
messageElement.appendChild(message.is_send ? messageContent : avatarTag);
messageElement.appendChild(message.is_send ? avatarTag : messageContent);
} }
} }
else if (message.type == 34) { else if (message.type == 34) {
if (message.is_send == 1) { // displayname 转的文字 audio
messageElement.className = "item item-right"; messageContent.className = `content-wrapper content-wrapper-${side}`;
messageElement.innerHTML = `<div class='chat-audio'>${message.voice_to_text == "" ? "" : `<div class="bubble">${message.voice_to_text}</div>`}<audio src="${formattedText}" controls></audio></div><div class='avatar'><img src="${message.avatar_path}" /></div>` if (message.is_chatroom && !message.is_send) {
messageContent.appendChild(displayNameBox(message));
} }
else if (message.is_send == 0) { if (message.voice_to_text) {
messageElement.className = "item item-left"; messageContent.appendChild(messageVoiceToTextBubble(message, side));
messageElement.innerHTML = `<div class='avatar'><img src="${message.avatar_path}" /></div><div class='chat-audio'>${message.voice_to_text == "" ? "" : `<div class="bubble">${message.voice_to_text}</div>`}<audio src="${formattedText}" controls></audio></div>`
} }
messageContent.appendChild(messageAudioBox(message));
// 整合
messageElement.className = `item item-${side}`;
messageElement.appendChild(message.is_send ? messageContent : avatarTag);
messageElement.appendChild(message.is_send ? avatarTag : messageContent);
} }
chatContainer.appendChild(messageElement); chatContainer.appendChild(messageElement);
if (message.type == 49 && message.sub_type == 57 && message.refer_text) {
chatContainer.appendChild(messageElementRefer);
}
} }
document.querySelector("#chat-container").scrollTop = 0; document.querySelector("#chat-container").scrollTop = 0;
updatePaginationInfo(); updatePaginationInfo();
refreshMediaListener();
} }
function prevPage() { function prevPage() {
@ -1062,6 +1257,39 @@ html_end = '''
// 初始化页面 // 初始化页面
renderPage(currentPage); renderPage(currentPage);
function refreshMediaListener() {
const audioTags = document.querySelectorAll('audio');
const videoTags = document.querySelectorAll('video');
audioTags.forEach(audio => {
audio.addEventListener('play', function () {
pauseOtherMedia(audio);
});
});
videoTags.forEach(video => {
video.addEventListener('play', function () {
pauseOtherMedia(video);
});
});
function pauseOtherMedia(currentMedia) {
const audioTags = document.querySelectorAll('audio');
const videoTags = document.querySelectorAll('video');
audioTags.forEach(media => {
if (media !== currentMedia && !media.paused) {
media.pause();
}
});
videoTags.forEach(media => {
if (media !== currentMedia && !media.paused) {
media.pause();
}
});
}
}
refreshMediaListener();
</script> </script>
<script> <script>

View File

@ -1,8 +1,9 @@
import threading import threading
from app.DataBase import msg_db, micro_msg_db from app.DataBase import msg_db, micro_msg_db, misc_db
from app.util.protocbuf.msg_pb2 import MessageBytesExtra from app.util.protocbuf.msg_pb2 import MessageBytesExtra
from app.util.protocbuf.roomdata_pb2 import ChatRoomData from app.util.protocbuf.roomdata_pb2 import ChatRoomData
from app.person import ContactPC, MePC
lock = threading.Lock() lock = threading.Lock()
@ -76,6 +77,63 @@ class PackageMsg:
row_list.append(sender) row_list.append(sender)
updated_messages.append(tuple(row_list)) updated_messages.append(tuple(row_list))
return updated_messages 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]: msg_sender, ContactPC类型这个才是群聊里的信息发送人不是群聊或者自己是发送者没有这个字段
'''
updated_messages = [] # 用于存储修改后的消息列表
messages = msg_db.get_messages(chatroom_wxid)
for row in messages:
message = list(row)
if message[4] == 1: # 自己发送的就没必要解析了
message.append(MePC())
updated_messages.append(message)
continue
if message[10] is None: # BytesExtra是空的跳过
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 不存在
updated_messages.append(message)
continue
contact_info_list = micro_msg_db.get_contact_by_username(wxid)
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 = ContactPC(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): def get_chatroom_member_list(self, strtalker):
membermap = {} membermap = {}
@ -101,3 +159,7 @@ class PackageMsg:
finally: finally:
lock.release() lock.release()
return membermap return membermap
if __name__ == "__main__":
p = PackageMsg()
print(p.get_package_message_by_wxid("44326600419@chatroom"))

View File

@ -48,7 +48,7 @@ def parser_reply(data: bytes):
'displayname': '用户名', 'displayname': '用户名',
} }
} }
try : try:
root = ET.XML(xml_content) root = ET.XML(xml_content)
appmsg = root.find('appmsg') appmsg = root.find('appmsg')
msg_type = int(appmsg.find('type').text) msg_type = int(appmsg.find('type').text)
@ -61,7 +61,7 @@ def parser_reply(data: bytes):
'title': escape_js_and_html(title), 'title': escape_js_and_html(title),
'refer': None if refermsg_type != 1 else { 'refer': None if refermsg_type != 1 else {
'type': refermsg_type, 'type': refermsg_type,
'content': escape_js_and_html(refermsg_content), 'content': escape_js_and_html(refermsg_content.lstrip("\n")),
'displayname': escape_js_and_html(refermsg_displayname), 'displayname': escape_js_and_html(refermsg_displayname),
} }
} }

View File

@ -11,6 +11,8 @@ emoji.py
import os import os
import traceback import traceback
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import sqlite3
import threading
import requests import requests
@ -43,7 +45,10 @@ def get_image_format(header):
def parser_xml(xml_string): def parser_xml(xml_string):
assert type(xml_string) == str assert type(xml_string) == str
# Parse the XML string # Parse the XML string
root = ET.fromstring(xml_string) try:
root = ET.fromstring(xml_string)
except:
root = ET.fromstring(xml_string.replace("&", "&amp;"))
emoji = root.find('./emoji') emoji = root.find('./emoji')
# Accessing attributes of the 'emoji' element # Accessing attributes of the 'emoji' element
fromusername = emoji.get('fromusername') fromusername = emoji.get('fromusername')
@ -61,9 +66,66 @@ def parser_xml(xml_string):
'height': height, 'height': height,
'cdnurl': cdnurl, 'cdnurl': cdnurl,
'thumburl': thumburl if thumburl else cdnurl, 'thumburl': thumburl if thumburl else cdnurl,
'md5': md5 if md5 else androidmd5, 'md5': (md5 if md5 else androidmd5).lower(),
} }
lock = threading.Lock()
db_path = "./app/Database/Msg/Emotion.db"
class Emotion:
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_emoji_url(self, md5: str):
sql = '''
select CDNUrl
from CustomEmotion
where md5 = ?
'''
try:
lock.acquire(True)
self.cursor.execute(sql, [md5])
return self.cursor.fetchone()[0]
except:
md5 = md5.upper()
sql = """
select Data
from EmotionItem
where md5 = ?
"""
self.cursor.execute(sql, [md5])
try:
return self.cursor.fetchone()[0]
except:
return ""
finally:
lock.release()
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()
@log @log
def download(url, output_dir, name, thumb=False): def download(url, output_dir, name, thumb=False):
if not url: if not url:
@ -95,9 +157,28 @@ def get_emoji(xml_string, thumb=True, output_path=root_path) -> str:
print('表情包已存在') print('表情包已存在')
return file_path return file_path
url = emoji_info['thumburl'] if thumb else emoji_info['cdnurl'] url = emoji_info['thumburl'] if thumb else emoji_info['cdnurl']
print("下载表情包ing:", url) if not url or url == "":
emoji_path = download(url, output_path, md5, thumb) url = Emotion().get_emoji_url(md5)
return emoji_path if type(url) == str and url != "":
print("下载表情包ing:", url)
emoji_path = download(url, output_path, md5, thumb)
return emoji_path
elif type(url) == bytes:
image_format = get_image_format(url[:8])
if image_format:
if thumb:
output_path = os.path.join(output_path, 'th_' + md5 + '.' + image_format)
else:
output_path = os.path.join(output_path, md5 + '.' + image_format)
else:
output_path = os.path.join(output_path, md5)
with open(output_path, 'wb') as f:
f.write(url)
print("表情包数据库加载", output_path)
return output_path
else:
print("!!!未知表情包数据,信息:", xml_string, emoji_info, url)
return ""
except: except:
logger.error(traceback.format_exc()) logger.error(traceback.format_exc())
return "" return ""

View File

@ -55,6 +55,7 @@ class ViewController(QWidget):
def close(self) -> bool: def close(self) -> bool:
close_db() close_db()
super().close() super().close()
if __name__ == '__main__': if __name__ == '__main__':
app = QApplication(sys.argv) app = QApplication(sys.argv)
font = QFont('微软雅黑', 12) # 使用 Times New Roman 字体,字体大小为 14 font = QFont('微软雅黑', 12) # 使用 Times New Roman 字体,字体大小为 14

View File

@ -16,4 +16,5 @@ google==3.0.0
protobuf==4.25.1 protobuf==4.25.1
soupsieve==2.5 soupsieve==2.5
lz4==4.3.2 lz4==4.3.2
pilk==0.2.4 pilk==0.2.4
eyed3==0.9.7