WeChatMsg/wxManager/model/message.py

654 lines
17 KiB
Python
Raw Normal View History

2025-03-28 21:29:18 +08:00
#!/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 # -11:语音通话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未知12
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未知12
'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