302 lines
8.7 KiB
Go
302 lines
8.7 KiB
Go
package model
|
||
|
||
import (
|
||
"fmt"
|
||
"strings"
|
||
"time"
|
||
|
||
"github.com/sjzar/chatlog/pkg/model/wxproto"
|
||
"github.com/sjzar/chatlog/pkg/util"
|
||
|
||
"google.golang.org/protobuf/proto"
|
||
)
|
||
|
||
const (
|
||
// Source
|
||
WeChatV3 = "wechatv3"
|
||
WeChatV4 = "wechatv4"
|
||
)
|
||
|
||
type Message struct {
|
||
Sequence int64 `json:"sequence"` // 消息序号,10位时间戳 + 3位序号
|
||
CreateTime time.Time `json:"createTime"` // 消息创建时间,10位时间戳
|
||
TalkerID int `json:"talkerID"` // 聊天对象,Name2ID 表序号,索引值
|
||
Talker string `json:"talker"` // 聊天对象,微信 ID or 群 ID
|
||
IsSender int `json:"isSender"` // 是否为发送消息,0 接收消息,1 发送消息
|
||
Type int `json:"type"` // 消息类型
|
||
SubType int `json:"subType"` // 消息子类型
|
||
Content string `json:"content"` // 消息内容,文字聊天内容 或 XML
|
||
CompressContent []byte `json:"compressContent"` // 非文字聊天内容,如图片、语音、视频等
|
||
IsChatRoom bool `json:"isChatRoom"` // 是否为群聊消息
|
||
ChatRoomSender string `json:"chatRoomSender"` // 群聊消息发送人
|
||
|
||
// Fill Info
|
||
// 从联系人等信息中填充
|
||
DisplayName string `json:"-"` // 显示名称
|
||
CharRoomName string `json:"-"` // 群聊名称
|
||
|
||
Version string `json:"-"` // 消息版本,内部判断
|
||
}
|
||
|
||
// CREATE TABLE MSG (
|
||
// localId INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
// TalkerId INT DEFAULT 0,
|
||
// MsgSvrID INT,
|
||
// Type INT,
|
||
// SubType INT,
|
||
// IsSender INT,
|
||
// CreateTime INT,
|
||
// Sequence INT DEFAULT 0,
|
||
// StatusEx INT DEFAULT 0,
|
||
// FlagEx INT,
|
||
// Status INT,
|
||
// MsgServerSeq INT,
|
||
// MsgSequence INT,
|
||
// StrTalker TEXT,
|
||
// StrContent TEXT,
|
||
// DisplayContent TEXT,
|
||
// Reserved0 INT DEFAULT 0,
|
||
// Reserved1 INT DEFAULT 0,
|
||
// Reserved2 INT DEFAULT 0,
|
||
// Reserved3 INT DEFAULT 0,
|
||
// Reserved4 TEXT,
|
||
// Reserved5 TEXT,
|
||
// Reserved6 TEXT,
|
||
// CompressContent BLOB,
|
||
// BytesExtra BLOB,
|
||
// BytesTrans BLOB
|
||
// )
|
||
type MessageV3 struct {
|
||
Sequence int64 `json:"Sequence"` // 消息序号,10位时间戳 + 3位序号
|
||
CreateTime int64 `json:"CreateTime"` // 消息创建时间,10位时间戳
|
||
TalkerID int `json:"TalkerId"` // 聊天对象,Name2ID 表序号,索引值
|
||
StrTalker string `json:"StrTalker"` // 聊天对象,微信 ID or 群 ID
|
||
IsSender int `json:"IsSender"` // 是否为发送消息,0 接收消息,1 发送消息
|
||
Type int `json:"Type"` // 消息类型
|
||
SubType int `json:"SubType"` // 消息子类型
|
||
StrContent string `json:"StrContent"` // 消息内容,文字聊天内容 或 XML
|
||
CompressContent []byte `json:"CompressContent"` // 非文字聊天内容,如图片、语音、视频等
|
||
BytesExtra []byte `json:"BytesExtra"` // protobuf 额外数据,记录群聊发送人等信息
|
||
|
||
// 非关键信息,后续有需要再加入
|
||
// LocalID int64 `json:"localId"`
|
||
// MsgSvrID int64 `json:"MsgSvrID"`
|
||
// StatusEx int `json:"StatusEx"`
|
||
// FlagEx int `json:"FlagEx"`
|
||
// Status int `json:"Status"`
|
||
// MsgServerSeq int64 `json:"MsgServerSeq"`
|
||
// MsgSequence int64 `json:"MsgSequence"`
|
||
// DisplayContent string `json:"DisplayContent"`
|
||
// Reserved0 int `json:"Reserved0"`
|
||
// Reserved1 int `json:"Reserved1"`
|
||
// Reserved2 int `json:"Reserved2"`
|
||
// Reserved3 int `json:"Reserved3"`
|
||
// Reserved4 string `json:"Reserved4"`
|
||
// Reserved5 string `json:"Reserved5"`
|
||
// Reserved6 string `json:"Reserved6"`
|
||
// BytesTrans []byte `json:"BytesTrans"`
|
||
}
|
||
|
||
func (m *MessageV3) Wrap() *Message {
|
||
|
||
isChatRoom := strings.HasSuffix(m.StrTalker, "@chatroom")
|
||
|
||
var chatRoomSender string
|
||
if len(m.BytesExtra) != 0 && isChatRoom {
|
||
chatRoomSender = ParseBytesExtra(m.BytesExtra)
|
||
}
|
||
|
||
return &Message{
|
||
Sequence: m.Sequence,
|
||
CreateTime: time.Unix(m.CreateTime, 0),
|
||
TalkerID: m.TalkerID,
|
||
Talker: m.StrTalker,
|
||
IsSender: m.IsSender,
|
||
Type: m.Type,
|
||
SubType: m.SubType,
|
||
Content: m.StrContent,
|
||
CompressContent: m.CompressContent,
|
||
IsChatRoom: isChatRoom,
|
||
ChatRoomSender: chatRoomSender,
|
||
Version: WeChatV3,
|
||
}
|
||
}
|
||
|
||
// CREATE TABLE Msg_xxxxxxxxxxxx(
|
||
// local_id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||
// server_id INTEGER,
|
||
// local_type INTEGER,
|
||
// sort_seq INTEGER,
|
||
// real_sender_id INTEGER,
|
||
// create_time INTEGER,
|
||
// status INTEGER,
|
||
// upload_status INTEGER,
|
||
// download_status INTEGER,
|
||
// server_seq INTEGER,
|
||
// origin_source INTEGER,
|
||
// source TEXT,
|
||
// message_content TEXT,
|
||
// compress_content TEXT,
|
||
// packed_info_data BLOB,
|
||
// WCDB_CT_message_content INTEGER DEFAULT NULL,
|
||
// WCDB_CT_source INTEGER DEFAULT NULL
|
||
// )
|
||
type MessageV4 struct {
|
||
SortSeq int64 `json:"sort_seq"` // 消息序号,10位时间戳 + 3位序号
|
||
LocalType int `json:"local_type"` // 消息类型
|
||
RealSenderID int `json:"real_sender_id"` // 发送人 ID,对应 Name2Id 表序号
|
||
CreateTime int64 `json:"create_time"` // 消息创建时间,10位时间戳
|
||
MessageContent []byte `json:"message_content"` // 消息内容,文字聊天内容 或 zstd 压缩内容
|
||
PackedInfoData []byte `json:"packed_info_data"` // 额外数据,类似 proto,格式与 v3 有差异
|
||
Status int `json:"status"` // 消息状态,2 是已发送,4 是已接收,可以用于判断 IsSender(猜测)
|
||
|
||
// 非关键信息,后续有需要再加入
|
||
// LocalID int `json:"local_id"`
|
||
// ServerID int64 `json:"server_id"`
|
||
|
||
// UploadStatus int `json:"upload_status"`
|
||
// DownloadStatus int `json:"download_status"`
|
||
// ServerSeq int `json:"server_seq"`
|
||
// OriginSource int `json:"origin_source"`
|
||
// Source string `json:"source"`
|
||
// CompressContent string `json:"compress_content"`
|
||
}
|
||
|
||
func (m *MessageV4) Wrap(id2Name map[int]string, isChatRoom bool) *Message {
|
||
|
||
_m := &Message{
|
||
Sequence: m.SortSeq,
|
||
CreateTime: time.Unix(m.CreateTime, 0),
|
||
TalkerID: m.RealSenderID, // 依赖 Name2Id 表进行转换为 StrTalker
|
||
CompressContent: m.PackedInfoData,
|
||
Type: m.LocalType,
|
||
Version: WeChatV4,
|
||
}
|
||
|
||
if name, ok := id2Name[m.RealSenderID]; ok {
|
||
_m.Talker = name
|
||
}
|
||
|
||
if m.Status == 2 {
|
||
_m.IsSender = 1
|
||
}
|
||
|
||
if util.IsNormalString(m.MessageContent) {
|
||
_m.Content = string(m.MessageContent)
|
||
} else {
|
||
_m.CompressContent = m.MessageContent
|
||
}
|
||
|
||
if isChatRoom {
|
||
_m.IsChatRoom = true
|
||
split := strings.Split(_m.Content, "\n")
|
||
if len(split) > 1 {
|
||
_m.Content = split[1]
|
||
_m.ChatRoomSender = strings.TrimSuffix(split[0], ":")
|
||
}
|
||
}
|
||
|
||
return _m
|
||
}
|
||
|
||
// ParseBytesExtra 解析额外数据
|
||
// 按需解析
|
||
func ParseBytesExtra(b []byte) (chatRoomSender string) {
|
||
var pbMsg wxproto.BytesExtra
|
||
if err := proto.Unmarshal(b, &pbMsg); err != nil {
|
||
return
|
||
}
|
||
if pbMsg.Items == nil {
|
||
return
|
||
}
|
||
|
||
for _, item := range pbMsg.Items {
|
||
if item.Type == 1 {
|
||
return item.Value
|
||
}
|
||
}
|
||
|
||
return
|
||
}
|
||
|
||
func (m *Message) PlainText(showChatRoom bool) string {
|
||
buf := strings.Builder{}
|
||
|
||
talker := m.Talker
|
||
if m.IsSender == 1 {
|
||
talker = "我"
|
||
} else if m.IsChatRoom {
|
||
talker = m.ChatRoomSender
|
||
}
|
||
if m.DisplayName != "" {
|
||
buf.WriteString(m.DisplayName)
|
||
buf.WriteString("(")
|
||
buf.WriteString(talker)
|
||
buf.WriteString(")")
|
||
} else {
|
||
buf.WriteString(talker)
|
||
}
|
||
buf.WriteString(" ")
|
||
|
||
if m.IsChatRoom && showChatRoom {
|
||
buf.WriteString("[")
|
||
if m.CharRoomName != "" {
|
||
buf.WriteString(m.CharRoomName)
|
||
buf.WriteString("(")
|
||
buf.WriteString(m.Talker)
|
||
buf.WriteString(")")
|
||
} else {
|
||
buf.WriteString(m.Talker)
|
||
}
|
||
buf.WriteString("] ")
|
||
}
|
||
|
||
buf.WriteString(m.CreateTime.Format("2006-01-02 15:04:05"))
|
||
buf.WriteString("\n")
|
||
|
||
switch m.Type {
|
||
case 1:
|
||
buf.WriteString(m.Content)
|
||
case 3:
|
||
buf.WriteString("[图片]")
|
||
case 34:
|
||
buf.WriteString("[语音]")
|
||
case 43:
|
||
buf.WriteString("[视频]")
|
||
case 47:
|
||
buf.WriteString("[动画表情]")
|
||
case 49:
|
||
switch m.SubType {
|
||
case 6:
|
||
buf.WriteString("[文件]")
|
||
case 8:
|
||
buf.WriteString("[GIF表情]")
|
||
case 19:
|
||
buf.WriteString("[合并转发]")
|
||
case 33, 36:
|
||
buf.WriteString("[小程序]")
|
||
case 57:
|
||
buf.WriteString("[引用]")
|
||
case 63:
|
||
buf.WriteString("[视频号]")
|
||
case 87:
|
||
buf.WriteString("[群公告]")
|
||
case 2000:
|
||
buf.WriteString("[转账]")
|
||
case 2003:
|
||
buf.WriteString("[红包封面]")
|
||
default:
|
||
buf.WriteString("[分享]")
|
||
}
|
||
case 50:
|
||
buf.WriteString("[语音通话]")
|
||
case 10000:
|
||
buf.WriteString("[系统消息]")
|
||
default:
|
||
buf.WriteString(fmt.Sprintf("Type: %d Content: %s", m.Type, m.Content))
|
||
}
|
||
buf.WriteString("\n")
|
||
|
||
return buf.String()
|
||
}
|