mirror of
https://github.com/LC044/WeChatMsg
synced 2025-04-17 00:48:23 +08:00
654 lines
17 KiB
Python
654 lines
17 KiB
Python
#!/usr/bin/env python
|
||
# -*- coding: utf-8 -*-
|
||
|
||
"""
|
||
@Time : 2024/12/10 21:03
|
||
@Author : SiYuan
|
||
@Email : 863909694@qq.com
|
||
@File : MemoTrace-message.py
|
||
@Description :
|
||
"""
|
||
from dataclasses import dataclass
|
||
from typing import List
|
||
from datetime import datetime
|
||
|
||
import xmltodict
|
||
|
||
|
||
class MessageType:
|
||
Unknown = -1
|
||
Text = 1
|
||
Text2 = 2
|
||
Image = 3
|
||
Audio = 34
|
||
BusinessCard = 42
|
||
Video = 43
|
||
Emoji = 47
|
||
Position = 48
|
||
Voip = 50
|
||
OpenIMBCard = 66
|
||
System = 10000
|
||
File = 25769803825
|
||
LinkMessage = 21474836529
|
||
LinkMessage2 = 292057776177
|
||
Music = 12884901937
|
||
LinkMessage4 = 4294967345
|
||
LinkMessage5 = 326417514545
|
||
LinkMessage6 = 17179869233
|
||
RedEnvelope = 8594229559345
|
||
Transfer = 8589934592049
|
||
Quote = 244813135921
|
||
MergedMessages = 81604378673
|
||
Applet = 141733920817
|
||
Applet2 = 154618822705
|
||
WeChatVideo = 219043332145
|
||
FavNote = 103079215153
|
||
Pat = 266287972401
|
||
|
||
@classmethod
|
||
def name(cls, type_):
|
||
type_name_map = {
|
||
cls.Unknown: '未知类型',
|
||
cls.Text: '文本',
|
||
cls.Image: '图片',
|
||
cls.Video: '视频',
|
||
cls.Audio: '语音',
|
||
cls.Emoji: '表情包',
|
||
cls.Voip: '音视频通话',
|
||
cls.File: '文件',
|
||
cls.Position: '位置分享',
|
||
cls.LinkMessage: '分享链接',
|
||
cls.LinkMessage2: '分享链接',
|
||
cls.LinkMessage4: '分享链接',
|
||
cls.LinkMessage5: '分享链接',
|
||
cls.LinkMessage6: '分享链接',
|
||
cls.RedEnvelope: '红包',
|
||
cls.Transfer: '转账',
|
||
cls.Quote: '引用消息',
|
||
cls.MergedMessages: '合并转发的聊天记录',
|
||
cls.Applet: '小程序',
|
||
cls.Applet2: '小程序',
|
||
cls.WeChatVideo: '视频号',
|
||
cls.Music: '音乐分享',
|
||
cls.FavNote: '收藏笔记',
|
||
cls.BusinessCard: '个人/公众号名片',
|
||
cls.OpenIMBCard: '企业微信名片',
|
||
cls.System: '系统消息',
|
||
cls.Pat: '拍一拍'
|
||
}
|
||
return type_name_map.get(type_, '未知类型')
|
||
|
||
|
||
@dataclass
|
||
class Message:
|
||
local_id: int # 消息ID
|
||
server_id: int # 消息的唯一ID
|
||
sort_seq: int # 排序用的id
|
||
timestamp: int # 发送秒级时间戳
|
||
str_time: str # 格式化时间 2024-12-01 12:00:00
|
||
type: MessageType # 消息类型(文本、图片、视频等)
|
||
talker_id: str # 聊天对象的wxid,好友的wxid或者群聊的wxid
|
||
is_sender: bool # 自己是否是发送者
|
||
sender_id: str # 消息发送者的ID
|
||
display_name: str # 消息发送者的对外展示的昵称(备注名,群昵称)
|
||
avatar_src: str # 消息发送者头像
|
||
status: int # 消息状态
|
||
xml_content: str # xml数据
|
||
|
||
def is_chatroom(self) -> bool:
|
||
return self.talker_id.endswith('@chatroom')
|
||
|
||
def to_json(self) -> dict:
|
||
try:
|
||
xml_dict = xmltodict.parse(self.xml_content)
|
||
except:
|
||
xml_dict = {}
|
||
return {
|
||
'type': str(self.type),
|
||
'is_send': self.is_sender,
|
||
'timestamp': self.timestamp,
|
||
'server_id': str(self.server_id),
|
||
'display_name': self.display_name,
|
||
'avatar_src': self.avatar_src,
|
||
'xml_dict': xml_dict
|
||
}
|
||
|
||
def type_name(self):
|
||
# 获取消息类型的文字描述
|
||
return MessageType.name(self.type)
|
||
|
||
def to_text(self):
|
||
try:
|
||
return f'{self.type}\n{xmltodict.parse(self.xml_content)}'
|
||
except:
|
||
print(self.xml_content)
|
||
return f'{self.type}\n{self.xml_content}'
|
||
|
||
def __lt__(self, other):
|
||
return self.sort_seq < other.sort_seq
|
||
|
||
|
||
@dataclass
|
||
class TextMessage(Message):
|
||
# 文本消息
|
||
content: str
|
||
|
||
def to_text(self):
|
||
return self.content
|
||
|
||
def to_json(self) -> dict:
|
||
data = super().to_json()
|
||
data['text'] = self.content
|
||
return data
|
||
|
||
|
||
@dataclass
|
||
class QuoteMessage(TextMessage):
|
||
# 引用消息
|
||
quote_message: Message
|
||
|
||
def to_json(self) -> dict:
|
||
data = super().to_json()
|
||
data.update(
|
||
{
|
||
"text": self.content,
|
||
'quote_server_id': f'{self.quote_message.server_id}',
|
||
'quote_type': self.quote_message.type,
|
||
}
|
||
)
|
||
if self.quote_message.type == MessageType.Quote:
|
||
# 防止递归引用
|
||
data['quote_text'] = f'{self.quote_message.display_name}: {self.quote_message.content}'
|
||
else:
|
||
data['quote_text'] = f'{self.quote_message.display_name}: {self.quote_message.to_text()}'
|
||
return data
|
||
|
||
def to_text(self):
|
||
if self.quote_message.type == MessageType.Quote:
|
||
# 防止递归引用
|
||
return f'{self.content}\n引用:{self.quote_message.display_name}: {self.quote_message.content}'
|
||
else:
|
||
return f'{self.content}\n引用:{self.quote_message.display_name}: {self.quote_message.to_text()}'
|
||
|
||
|
||
@dataclass
|
||
class FileMessage(Message):
|
||
# 文件消息
|
||
path: str
|
||
md5: str
|
||
file_size: int
|
||
file_name: str
|
||
file_type: str
|
||
|
||
def to_json(self) -> dict:
|
||
data = super().to_json()
|
||
data.update(
|
||
{
|
||
'path': self.path,
|
||
'file_name': self.file_name,
|
||
'file_size': self.file_size,
|
||
'file_type': self.file_type
|
||
}
|
||
)
|
||
return data
|
||
|
||
def get_file_size(self, format_='MB'):
|
||
# 定义转换因子
|
||
units = {
|
||
'B': 1,
|
||
'KB': 1024,
|
||
'MB': 1024 ** 2,
|
||
'GB': 1024 ** 3,
|
||
}
|
||
|
||
# 将文件大小转换为指定格式
|
||
if format_ in units:
|
||
size_in_format = self.file_size / units[format_]
|
||
return f'{size_in_format:.2f} {format_}'
|
||
else:
|
||
raise ValueError(f'Unsupported format: {format_}')
|
||
|
||
def set_file_name(self, file_name=''):
|
||
if file_name:
|
||
self.file_name = file_name
|
||
return True
|
||
# 把时间戳转换为格式化时间
|
||
time_struct = datetime.fromtimestamp(self.timestamp) # 首先把时间戳转换为结构化时间
|
||
str_time = time_struct.strftime("%Y%m%d_%H%M%S") # 把结构化时间转换为格式化时间
|
||
str_time = f'{str_time}_{str(self.server_id)[:6]}'
|
||
if self.is_sender:
|
||
str_time += '_1'
|
||
else:
|
||
str_time += '_0'
|
||
self.file_name = str_time
|
||
return True
|
||
|
||
def to_text(self):
|
||
return f'【文件】{self.file_name} {self.get_file_size()} {self.path} {self.file_type} {self.md5}'
|
||
|
||
|
||
@dataclass
|
||
class ImageMessage(FileMessage):
|
||
# 图片消息
|
||
thumb_path: str
|
||
|
||
def to_json(self) -> dict:
|
||
data = super().to_json()
|
||
data['path'] = self.path
|
||
data['thumb_path'] = self.thumb_path
|
||
return data
|
||
|
||
def to_text(self):
|
||
return f'【图片】'
|
||
|
||
|
||
@dataclass
|
||
class EmojiMessage(ImageMessage):
|
||
# 表情包
|
||
url: str
|
||
thumb_url: str
|
||
description: str
|
||
|
||
def to_json(self) -> dict:
|
||
data = super().to_json()
|
||
data.update(
|
||
{
|
||
'path': self.url,
|
||
'desc': self.description
|
||
}
|
||
)
|
||
return data
|
||
|
||
def to_text(self):
|
||
return f'【表情包】 {self.description}'
|
||
|
||
|
||
@dataclass
|
||
class VideoMessage(FileMessage):
|
||
# 视频消息
|
||
thumb_path: str
|
||
duration: int
|
||
raw_md5: str
|
||
|
||
def to_text(self):
|
||
return '【视频】'
|
||
|
||
def to_json(self) -> dict:
|
||
data = super().to_json()
|
||
data.update(
|
||
{
|
||
'path': self.path,
|
||
'thumb_path': self.thumb_path,
|
||
'duration': self.duration
|
||
}
|
||
)
|
||
return data
|
||
|
||
|
||
@dataclass
|
||
class AudioMessage(FileMessage):
|
||
# 语音消息
|
||
duration: int
|
||
audio_text: str
|
||
|
||
def set_file_name(self):
|
||
# 把时间戳转换为格式化时间
|
||
time_struct = datetime.fromtimestamp(self.timestamp) # 首先把时间戳转换为结构化时间
|
||
str_time = time_struct.strftime("%Y%m%d_%H%M%S") # 把结构化时间转换为格式化时间
|
||
str_time = f'{str_time}_{str(self.server_id)[:6]}'
|
||
if self.is_sender:
|
||
str_time += '_1'
|
||
else:
|
||
str_time += '_0'
|
||
self.file_name = str_time
|
||
|
||
def get_file_name(self):
|
||
return self.file_name
|
||
|
||
def to_json(self) -> dict:
|
||
data = super().to_json()
|
||
data.update(
|
||
{
|
||
'path': self.path,
|
||
'voice_to_text': self.audio_text,
|
||
'duration': self.duration,
|
||
}
|
||
)
|
||
return data
|
||
|
||
def to_text(self):
|
||
# return f'{self.server_id}\n{self.type}\n{xmltodict.parse(self.xml_content)}'
|
||
return f'【语音】{self.audio_text}'
|
||
|
||
|
||
@dataclass
|
||
class LinkMessage(Message):
|
||
# 链接消息
|
||
href: str # 跳转链接
|
||
title: str # 标题
|
||
description: str # 描述/音乐作者
|
||
cover_path: str # 本地封面路径
|
||
cover_url: str # 封面地址
|
||
app_name: str # 应用名
|
||
app_icon: str # 应用logo
|
||
app_id: str # app ip
|
||
|
||
def to_text(self):
|
||
return f'''【分享链接】
|
||
标题:{self.title}
|
||
描述:{self.description}
|
||
链接: {self.href}
|
||
应用:{self.app_name}
|
||
'''
|
||
|
||
def to_json(self) -> dict:
|
||
data = super().to_json()
|
||
data.update(
|
||
{
|
||
'url': self.href,
|
||
'title': self.title,
|
||
'description': self.description,
|
||
'cover_url': self.cover_url,
|
||
'app_logo': self.app_icon,
|
||
'app_name': self.app_name,
|
||
}
|
||
)
|
||
return data
|
||
|
||
|
||
@dataclass
|
||
class WeChatVideoMessage(Message):
|
||
# 视频号消息
|
||
url: str # 下载地址
|
||
publisher_nickname: str # 视频发布者昵称
|
||
publisher_avatar: str # 视频发布者头像
|
||
description: str # 视频描述
|
||
media_count: int # 视频个数
|
||
cover_path: str # 封面本地路径
|
||
cover_url: str # 封面网址
|
||
thumb_url: str # 缩略图
|
||
duration: int # 视频时长,单位(秒)
|
||
width: int # 视频宽度
|
||
height: int # 视频高度
|
||
|
||
def to_text(self):
|
||
return f'''【视频号】
|
||
描述: {self.description}
|
||
发布者: {self.publisher_nickname}
|
||
'''
|
||
|
||
def to_json(self) -> dict:
|
||
data = super().to_json()
|
||
data.update(
|
||
{
|
||
'url': self.url,
|
||
'title': self.description,
|
||
'cover_url': self.cover_url,
|
||
'duration': self.duration,
|
||
'publisher_nickname': self.publisher_nickname,
|
||
'publisher_avatar': self.publisher_avatar
|
||
}
|
||
)
|
||
return data
|
||
|
||
|
||
@dataclass
|
||
class MergedMessage(Message):
|
||
# 合并转发的聊天记录
|
||
title: str
|
||
description: str
|
||
messages: List[Message] # 嵌套子消息
|
||
level: int # 嵌套层数
|
||
|
||
def to_text(self):
|
||
res = f'【合并转发的聊天记录】\n\n'
|
||
for message in self.messages:
|
||
res += f"{' ' * self.level * 4}- {message.str_time} {message.display_name}: {message.to_text()}\n"
|
||
return res
|
||
|
||
def to_json(self) -> dict:
|
||
data = super().to_json()
|
||
data.update(
|
||
{
|
||
'title': self.title,
|
||
'description': self.description,
|
||
'messages': [msg.to_json() for msg in self.messages],
|
||
}
|
||
)
|
||
return data
|
||
|
||
|
||
@dataclass
|
||
class VoipMessage(Message):
|
||
# 音视频通话
|
||
invite_type: int # -1,1:语音通话,0:视频通话
|
||
display_content: str # 界面显示内容
|
||
duration: int
|
||
|
||
def to_text(self):
|
||
return f'【音视频通话】\n{self.display_content}'
|
||
|
||
def to_json(self) -> dict:
|
||
data = super().to_json()
|
||
data.update(
|
||
{
|
||
'invite_type': self.invite_type,
|
||
'display_content': self.display_content,
|
||
'duration': self.duration
|
||
}
|
||
)
|
||
return data
|
||
|
||
|
||
@dataclass
|
||
class PositionMessage(Message):
|
||
# 位置分享
|
||
x: float # 经度
|
||
y: float # 维度
|
||
label: str # 详细标签
|
||
poiname: str # 位置点标记名
|
||
scale: float # 缩放率
|
||
|
||
def to_text(self):
|
||
return f'''【位置分享】
|
||
坐标: ({self.x},{self.y})
|
||
名称: {self.poiname}
|
||
标签: {self.label}
|
||
'''
|
||
|
||
def to_json(self) -> dict:
|
||
data = super().to_json()
|
||
data.update(
|
||
{
|
||
'x': self.x, # 经度
|
||
'y': self.y, # 维度
|
||
'label': self.label, # 详细标签
|
||
'poiname': self.poiname, # 位置点标记名
|
||
'scale': self.scale, # 缩放率
|
||
}
|
||
)
|
||
return data
|
||
|
||
|
||
@dataclass
|
||
class BusinessCardMessage(Message):
|
||
# 名片消息
|
||
is_open_im: bool # 是否是企业微信
|
||
username: str # 名片的wxid
|
||
nickname: str # 名片昵称
|
||
alias: str # 名片微信号
|
||
province: str # 省份
|
||
city: str # 城市
|
||
sign: str # 签名
|
||
sex: int # 性别 0:未知,1:男,2:女
|
||
small_head_url: str # 头像
|
||
big_head_url: str # 头像原图
|
||
open_im_desc: str # 公司名
|
||
open_im_desc_icon: str # 公司logo
|
||
|
||
def _sex_name(self):
|
||
if self.sex == 0:
|
||
return '未知'
|
||
elif self.sex == 1:
|
||
return '男'
|
||
else:
|
||
return '女'
|
||
|
||
def to_text(self):
|
||
if self.is_open_im:
|
||
return f'''【名片】
|
||
公司: {self.open_im_desc}
|
||
昵称: {self.nickname}
|
||
性别: {self._sex_name()}
|
||
'''
|
||
else:
|
||
return f'''【名片】
|
||
微信号:{self.alias}
|
||
昵称: {self.nickname}
|
||
签名: {self.sign}
|
||
性别: {self._sex_name()}
|
||
地区: {self.province} {self.city}
|
||
'''
|
||
|
||
def to_json(self) -> dict:
|
||
data = super().to_json()
|
||
data.update(
|
||
{
|
||
'is_open_im': self.is_open_im,
|
||
'big_head_url': self.big_head_url, # 头像原图
|
||
'small_head_url': self.small_head_url, # 小头像
|
||
'username': self.username, # wxid
|
||
'nickname': self.nickname, # 昵称
|
||
'alias': self.alias, # 微信号
|
||
'province': self.province, # 省份
|
||
'city': self.city, # 城市
|
||
'sex': self._sex_name(), # int :性别 0:未知,1:男,2:女
|
||
'open_im_desc': self.open_im_desc, # 公司名
|
||
'open_im_desc_icon': self.open_im_desc_icon, # 公司名前面的图标
|
||
}
|
||
)
|
||
return data
|
||
|
||
|
||
@dataclass
|
||
class TransferMessage(Message):
|
||
# 转账
|
||
fee_desc: str # 金额
|
||
pay_memo: str # 备注
|
||
receiver_username: str # 收款人
|
||
pay_subtype: int # 状态
|
||
|
||
def display_content(self):
|
||
text_info_map = {
|
||
1: "发起转账",
|
||
3: "已收款",
|
||
4: "已退还",
|
||
5: "非实时转账收款",
|
||
7: "发起非实时转账",
|
||
8: "未知",
|
||
9: "未知",
|
||
}
|
||
return text_info_map.get(self.pay_subtype, '未知')
|
||
|
||
def to_text(self):
|
||
return f'''【{self.display_content()}】:{self.fee_desc}
|
||
备注: {self.pay_memo}
|
||
'''
|
||
|
||
def to_json(self) -> dict:
|
||
data = super().to_json()
|
||
data.update(
|
||
{
|
||
'text': self.display_content(), # 显示文本
|
||
'pay_subtype': self.pay_subtype, # 当前状态
|
||
'pay_memo': self.pay_memo, # 备注
|
||
'fee_desc': self.fee_desc # 金额
|
||
}
|
||
)
|
||
return data
|
||
|
||
|
||
@dataclass
|
||
class RedEnvelopeMessage(Message):
|
||
# 红包
|
||
icon_url: str # 红包logo
|
||
title: str
|
||
inner_type: int
|
||
|
||
def to_text(self):
|
||
return f'''【红包】: {self.title}'''
|
||
|
||
def to_json(self) -> dict:
|
||
data = super().to_json()
|
||
data.update(
|
||
{
|
||
'text': self.title, # 显示文本
|
||
'inner_type': self.inner_type, # 当前状态
|
||
}
|
||
)
|
||
return data
|
||
|
||
|
||
@dataclass
|
||
class FavNoteMessage(Message):
|
||
# 收藏笔记
|
||
title: str
|
||
description: str
|
||
record_item: str
|
||
|
||
def to_text(self):
|
||
return f'''【笔记】
|
||
{self.description}
|
||
{self.record_item}
|
||
'''
|
||
|
||
def to_json(self) -> dict:
|
||
data = super().to_json()
|
||
data.update(
|
||
{
|
||
'text': self.title, # 显示文本
|
||
'description': self.description, # 内容
|
||
'record_item': self.record_item
|
||
}
|
||
)
|
||
return data
|
||
|
||
|
||
@dataclass
|
||
class PatMessage(Message):
|
||
# 拍一拍
|
||
title: str
|
||
from_username: str
|
||
chat_username: str
|
||
patted_username: str
|
||
template: str
|
||
|
||
def to_text(self):
|
||
return self.title
|
||
|
||
def to_json(self) -> dict:
|
||
data = super().to_json()
|
||
data.update(
|
||
{
|
||
'type': MessageType.System,
|
||
'text': self.title, # 显示文本
|
||
}
|
||
)
|
||
return data
|
||
|
||
|
||
if __name__ == '__main__':
|
||
msg = TextMessage(
|
||
local_id=1,
|
||
server_id=101,
|
||
timestamp=1678901234,
|
||
type="text",
|
||
talker_id="wxid_12345",
|
||
is_sender=True,
|
||
sender_id="wxid_67890",
|
||
display_name="John Doe",
|
||
status=3,
|
||
content="Hello, world!"
|
||
)
|
||
print(msg.status) # 输出:3
|