From b4378a63a336026e0d23868714a1f5ec0fcbab43 Mon Sep 17 00:00:00 2001 From: Sarv Date: Wed, 9 Apr 2025 00:02:55 +0800 Subject: [PATCH] adjust message handing (#22) * adjust message handing * mcp required args --- internal/chatlog/mcp/const.go | 3 + internal/chatlog/mcp/service.go | 6 - internal/mcp/tool.go | 5 +- internal/model/mediamessage.go | 450 +++++++++--------- internal/model/message.go | 439 ++++++++++------- internal/model/message_darwinv3.go | 39 +- internal/model/message_v3.go | 117 +++++ internal/model/message_v4.go | 61 +-- internal/wechatdb/datasource/v4/datasource.go | 49 +- .../datasource/windowsv3/datasource.go | 8 +- internal/wechatdb/repository/message.go | 16 +- 11 files changed, 688 insertions(+), 505 deletions(-) create mode 100644 internal/model/message_v3.go diff --git a/internal/chatlog/mcp/const.go b/internal/chatlog/mcp/const.go index 35ffe63..64ba277 100644 --- a/internal/chatlog/mcp/const.go +++ b/internal/chatlog/mcp/const.go @@ -26,6 +26,7 @@ var ( "description": "联系人的搜索关键词,可以是姓名、备注名或ID。", }, }, + Required: []string{"query"}, }, } @@ -40,6 +41,7 @@ var ( "description": "群聊的搜索关键词,可以是群名称、群ID或相关描述", }, }, + Required: []string{"query"}, }, } @@ -67,6 +69,7 @@ var ( "description": "交谈对象,可以是联系人或群聊。支持使用ID、昵称、备注名等进行查询。", }, }, + Required: []string{"time", "talker"}, }, } diff --git a/internal/chatlog/mcp/service.go b/internal/chatlog/mcp/service.go index 82f1126..8ebff79 100644 --- a/internal/chatlog/mcp/service.go +++ b/internal/chatlog/mcp/service.go @@ -191,9 +191,6 @@ func (s *Service) toolsCall(session *mcp.Session, req *mcp.Request) error { talker = v.(string) } limit := util.MustAnyToInt(callReq.Arguments["limit"]) - if limit == 0 { - limit = 100 - } offset := util.MustAnyToInt(callReq.Arguments["offset"]) messages, err := s.db.GetMessages(start, end, talker, limit, offset) if err != nil { @@ -264,9 +261,6 @@ func (s *Service) resourcesRead(session *mcp.Session, req *mcp.Request) error { return fmt.Errorf("无法解析时间范围") } limit := util.MustAnyToInt(u.Query().Get("limit")) - if limit == 0 { - limit = 100 - } offset := util.MustAnyToInt(u.Query().Get("offset")) messages, err := s.db.GetMessages(start, end, u.Host, limit, offset) if err != nil { diff --git a/internal/mcp/tool.go b/internal/mcp/tool.go index 5cf3cce..1975740 100644 --- a/internal/mcp/tool.go +++ b/internal/mcp/tool.go @@ -92,8 +92,9 @@ type Tool struct { } type ToolSchema struct { - Type string `json:"type"` - Properties M `json:"properties"` + Type string `json:"type"` + Properties M `json:"properties"` + Required []string `json:"required,omitempty"` } // { diff --git a/internal/model/mediamessage.go b/internal/model/mediamessage.go index 34249a9..b8f7254 100644 --- a/internal/model/mediamessage.go +++ b/internal/model/mediamessage.go @@ -3,217 +3,17 @@ package model import ( "encoding/xml" "fmt" + "regexp" "strings" - "time" - - "github.com/sjzar/chatlog/pkg/util" ) -type MediaMessage struct { - Type int64 - SubType int - MediaMD5 string - MediaPath string - Title string - Desc string - Content string - URL string - - RecordInfo *RecordInfo - - ReferDisplayName string - ReferUserName string - ReferCreateTime time.Time - ReferMessage *MediaMessage - - Host string - - Message XMLMessage -} - -func NewMediaMessage(_type int64, data string) (*MediaMessage, error) { - - __type, subType := util.SplitInt64ToTwoInt32(_type) - - m := &MediaMessage{ - Type: __type, - SubType: int(subType), - } - - if _type == 1 { - m.Content = data - return m, nil - } - - var msg XMLMessage - err := xml.Unmarshal([]byte(data), &msg) - if err != nil { - return nil, err - } - - m.Message = msg - if err := m.parse(); err != nil { - return nil, err - } - - return m, nil -} - -func (m *MediaMessage) parse() error { - - switch m.Type { - case 3: - m.MediaMD5 = m.Message.Image.MD5 - case 43: - m.MediaMD5 = m.Message.Video.RawMd5 - case 49: - m.SubType = m.Message.App.Type - switch m.SubType { - case 5: - m.Title = m.Message.App.Title - m.URL = m.Message.App.URL - case 6: - m.Title = m.Message.App.Title - m.MediaMD5 = m.Message.App.MD5 - case 19: - m.Title = m.Message.App.Title - m.Desc = m.Message.App.Des - if m.Message.App.RecordItem == nil { - break - } - recordInfo := &RecordInfo{} - err := xml.Unmarshal([]byte(m.Message.App.RecordItem.CDATA), recordInfo) - if err != nil { - return err - } - m.RecordInfo = recordInfo - case 57: - m.Content = m.Message.App.Title - if m.Message.App.ReferMsg == nil { - break - } - subMsg, err := NewMediaMessage(m.Message.App.ReferMsg.Type, m.Message.App.ReferMsg.Content) - if err != nil { - break - } - m.ReferDisplayName = m.Message.App.ReferMsg.DisplayName - m.ReferUserName = m.Message.App.ReferMsg.ChatUsr - m.ReferCreateTime = time.Unix(m.Message.App.ReferMsg.CreateTime, 0) - m.ReferMessage = subMsg - } - } - - return nil -} - -func (m *MediaMessage) SetHost(host string) { - m.Host = host -} - -func (m *MediaMessage) String() string { - switch m.Type { - case 1: - return m.Content - case 3: - return fmt.Sprintf("![图片](http://%s/image/%s)", m.Host, m.MediaMD5) - case 34: - return "[语音]" - case 43: - if m.MediaPath != "" { - return fmt.Sprintf("![视频](http://%s/data/%s)", m.Host, m.MediaPath) - } - return fmt.Sprintf("![视频](http://%s/video/%s)", m.Host, m.MediaMD5) - case 47: - return "[动画表情]" - case 49: - switch m.SubType { - case 5: - return fmt.Sprintf("[链接|%s](%s)", m.Title, m.URL) - case 6: - return fmt.Sprintf("[文件|%s](http://%s/file/%s)", m.Title, m.Host, m.MediaMD5) - case 8: - return "[GIF表情]" - case 19: - if m.RecordInfo == nil { - return "[合并转发]" - } - buf := strings.Builder{} - for _, item := range m.RecordInfo.DataList.DataItems { - buf.WriteString(item.SourceName + ": ") - switch item.DataType { - case "jpg": - buf.WriteString(fmt.Sprintf("![图片](http://%s/image/%s)", m.Host, item.FullMD5)) - default: - buf.WriteString(item.DataDesc) - } - buf.WriteString("\n") - } - return m.Content - case 33, 36: - return "[小程序]" - case 57: - if m.ReferMessage == nil { - if m.Content == "" { - return "[引用]" - } - return "> [引用]\n" + m.Content - } - buf := strings.Builder{} - buf.WriteString("> ") - if m.ReferDisplayName != "" { - buf.WriteString(m.ReferDisplayName) - buf.WriteString("(") - buf.WriteString(m.ReferUserName) - buf.WriteString(")") - } else { - buf.WriteString(m.ReferUserName) - } - buf.WriteString(" ") - buf.WriteString(m.ReferCreateTime.Format("2006-01-02 15:04:05")) - buf.WriteString("\n") - buf.WriteString("> ") - m.ReferMessage.SetHost(m.Host) - buf.WriteString(strings.ReplaceAll(m.ReferMessage.String(), "\n", "\n> ")) - buf.WriteString("\n") - buf.WriteString(m.Content) - m.Content = buf.String() - return m.Content - case 63: - return "[视频号]" - case 87: - return "[群公告]" - case 2000: - return "[转账]" - case 2003: - return "[红包封面]" - default: - return "[分享]" - } - case 50: - return "[语音通话]" - case 10000: - return "[系统消息]" - default: - content := m.Content - if len(content) > 120 { - content = content[:120] + "<...>" - } - return fmt.Sprintf("Type: %d Content: %s", m.Type, content) - } -} - -type XMLMessage struct { +type MediaMsg struct { XMLName xml.Name `xml:"msg"` Image Image `xml:"img,omitempty"` Video Video `xml:"videomsg,omitempty"` App App `xml:"appmsg,omitempty"` } -type XMLImageMessage struct { - XMLName xml.Name `xml:"msg"` - Img Image `xml:"img"` -} - type Image struct { MD5 string `xml:"md5,attr"` // HdLength string `xml:"hdlength,attr"` @@ -234,11 +34,6 @@ type Image struct { // CdnThumbAesKey string `xml:"cdnthumbaeskey,attr"` } -type XMLVideoMessage struct { - XMLName xml.Name `xml:"msg"` - VideoMsg Video `xml:"videomsg"` -} - type Video struct { RawMd5 string `xml:"rawmd5,attr"` // Length string `xml:"length,attr"` @@ -263,14 +58,18 @@ type Video struct { } type App struct { - Type int `xml:"type"` - Title string `xml:"title"` - Des string `xml:"des"` - URL string `xml:"url"` // type 5 分享 - AppAttach AppAttach `xml:"appattach"` // type 6 文件 - MD5 string `xml:"md5"` // type 6 文件 - RecordItem *RecordItem `xml:"recorditem,omitempty"` // type 19 合并转发 - ReferMsg *ReferMsg `xml:"refermsg,omitempty"` // type 57 引用 + Type int `xml:"type"` + Title string `xml:"title"` + Des string `xml:"des"` + URL string `xml:"url"` // type 5 分享 + AppAttach *AppAttach `xml:"appattach,omitempty"` // type 6 文件 + MD5 string `xml:"md5,omitempty"` // type 6 文件 + RecordItem *RecordItem `xml:"recorditem,omitempty"` // type 19 合并转发 + SourceDisplayName string `xml:"sourcedisplayname,omitempty"` // type 33 小程序 + FinderFeed *FinderFeed `xml:"finderFeed,omitempty"` // type 51 视频号 + ReferMsg *ReferMsg `xml:"refermsg,omitempty"` // type 57 引用 + PatMsg *PatMsg `xml:"patMsg,omitempty"` // type 62 拍一拍 + WCPayInfo *WCPayInfo `xml:"wcpayinfo,omitempty"` // type 2000 微信转账 } // ReferMsg 表示引用消息 @@ -352,4 +151,225 @@ type DataItem struct { SrcMsgCreateTime string `xml:"srcMsgCreateTime,omitempty"` MessageUUID string `xml:"messageuuid,omitempty"` FromNewMsgID string `xml:"fromnewmsgid,omitempty"` + + // 套娃合并转发 + DataTitle string `xml:"datatitle,omitempty"` + RecordXML *RecordXML `xml:"recordxml,omitempty"` +} + +type RecordXML struct { + RecordInfo RecordInfo `xml:"recordinfo,omitempty"` +} + +func (r *RecordInfo) String(title, host string) string { + buf := strings.Builder{} + if title == "" { + title = r.Title + } + buf.WriteString(fmt.Sprintf("[合并转发|%s]\n", title)) + for _, item := range r.DataList.DataItems { + buf.WriteString(fmt.Sprintf(" %s %s\n", item.SourceName, item.SourceTime)) + + // 套娃合并转发 + if item.DataType == "17" && item.RecordXML != nil { + content := item.RecordXML.RecordInfo.String(item.DataTitle, host) + if content != "" { + for _, line := range strings.Split(content, "\n") { + buf.WriteString(fmt.Sprintf(" %s\n", line)) + } + } + continue + } + + switch item.DataFmt { + case "pic", "jpg": + buf.WriteString(fmt.Sprintf(" ![图片](http://%s/image/%s)\n", host, item.FullMD5)) + default: + for _, line := range strings.Split(item.DataDesc, "\n") { + buf.WriteString(fmt.Sprintf(" %s\n", line)) + } + } + buf.WriteString("\n") + } + return buf.String() +} + +// PatMsg 拍一拍消息结构 +type PatMsg struct { + ChatUser string `xml:"chatUser"` // 被拍的用户 + RecordNum int `xml:"recordNum"` // 记录数量 + Records Records `xml:"records"` // 拍一拍记录 +} + +// Records 拍一拍记录集合 +type Records struct { + Record []PatRecord `xml:"record"` // 拍一拍记录列表 +} + +// PatRecord 单条拍一拍记录 +type PatRecord struct { + FromUser string `xml:"fromUser"` // 发起拍一拍的用户 + PattedUser string `xml:"pattedUser"` // 被拍的用户 + Templete string `xml:"templete"` // 模板文本 + CreateTime int64 `xml:"createTime"` // 创建时间 + SvrId string `xml:"svrId"` // 服务器ID + ReadStatus int `xml:"readStatus"` // 已读状态 +} + +// WCPayInfo 微信支付信息 +type WCPayInfo struct { + PaySubType int `xml:"paysubtype"` // 支付子类型 + FeeDesc string `xml:"feedesc"` // 金额描述,如"¥200000.00" + TranscationID string `xml:"transcationid"` // 交易ID + TransferID string `xml:"transferid"` // 转账ID + InvalidTime string `xml:"invalidtime"` // 失效时间 + BeginTransferTime string `xml:"begintransfertime"` // 开始转账时间 + EffectiveDate string `xml:"effectivedate"` // 生效日期 + PayMemo string `xml:"pay_memo"` // 支付备注 + ReceiverUsername string `xml:"receiver_username"` // 接收方用户名 + PayerUsername string `xml:"payer_username"` // 支付方用户名 +} + +// FinderFeed 视频号信息 +type FinderFeed struct { + ObjectID string `xml:"objectId"` + FeedType string `xml:"feedType"` + Nickname string `xml:"nickname"` + Avatar string `xml:"avatar"` + Desc string `xml:"desc"` + MediaCount string `xml:"mediaCount"` + ObjectNonceID string `xml:"objectNonceId"` + LiveID string `xml:"liveId"` + Username string `xml:"username"` + AuthIconURL string `xml:"authIconUrl"` + AuthIconType int `xml:"authIconType"` + ContactJumpInfoStr string `xml:"contactJumpInfoStr"` + SourceCommentScene int `xml:"sourceCommentScene"` + MediaList FinderMediaList `xml:"mediaList"` + MegaVideo FinderMegaVideo `xml:"megaVideo"` + BizUsername string `xml:"bizUsername"` + BizNickname string `xml:"bizNickname"` + BizAvatar string `xml:"bizAvatar"` + BizUsernameV2 string `xml:"bizUsernameV2"` + BizAuthIconURL string `xml:"bizAuthIconUrl"` + BizAuthIconType int `xml:"bizAuthIconType"` + EcSource string `xml:"ecSource"` + LastGMsgID string `xml:"lastGMsgID"` + ShareBypData string `xml:"shareBypData"` + IsDebug int `xml:"isDebug"` + ContentType int `xml:"content_type"` + FinderForwardSource string `xml:"finderForwardSource"` +} + +type FinderMediaList struct { + Media []FinderMedia `xml:"media"` +} + +type FinderMedia struct { + ThumbURL string `xml:"thumbUrl"` + FullCoverURL string `xml:"fullCoverUrl"` + VideoPlayDuration string `xml:"videoPlayDuration"` + URL string `xml:"url"` + CoverURL string `xml:"coverUrl"` + Height string `xml:"height"` + MediaType string `xml:"mediaType"` + FullClipInset string `xml:"fullClipInset"` + Width string `xml:"width"` +} + +type FinderMegaVideo struct { + ObjectID string `xml:"objectId"` + ObjectNonceID string `xml:"objectNonceId"` +} + +type SysMsg struct { + SysMsgTemplate SysMsgTemplate `xml:"sysmsgtemplate"` +} + +type SysMsgTemplate struct { + ContentTemplate ContentTemplate `xml:"content_template"` +} + +type ContentTemplate struct { + Type string `xml:"type,attr"` + Plain string `xml:"plain"` + Template string `xml:"template"` + LinkList LinkList `xml:"link_list"` +} + +type LinkList struct { + Links []Link `xml:"link"` +} + +type Link struct { + Name string `xml:"name,attr"` + Type string `xml:"type,attr"` + MemberList MemberList `xml:"memberlist"` + Separator string `xml:"separator"` +} + +type MemberList struct { + Members []Member `xml:"member"` +} + +type Member struct { + Username string `xml:"username"` + Nickname string `xml:"nickname"` +} + +func (s *SysMsg) String() string { + template := s.SysMsgTemplate.ContentTemplate.Template + links := s.SysMsgTemplate.ContentTemplate.LinkList.Links + + // 创建一个映射,用于存储占位符名称和对应的替换内容 + replacements := make(map[string]string) + + // 遍历所有链接,为每个占位符准备替换内容 + for _, link := range links { + var replacement string + + // 根据链接类型和成员信息生成替换内容 + switch link.Type { + case "link_profile": + // 使用自定义分隔符,如果未指定则默认使用"、" + separator := link.Separator + if separator == "" { + separator = "、" + } + + // 处理成员信息,格式为 nickname(username) + var memberTexts []string + for _, member := range link.MemberList.Members { + if member.Nickname != "" { + memberText := member.Nickname + if member.Username != "" { + memberText += "(" + member.Username + ")" + } + memberTexts = append(memberTexts, memberText) + } + } + + // 使用指定的分隔符连接所有成员文本 + replacement = strings.Join(memberTexts, separator) + + // 可以根据需要添加其他链接类型的处理逻辑 + default: + replacement = "" + } + + // 将占位符名称和替换内容存入映射 + replacements["$"+link.Name+"$"] = replacement + } + + // 使用正则表达式查找并替换所有占位符 + re := regexp.MustCompile(`\$([^$]+)\$`) + result := re.ReplaceAllStringFunc(template, func(match string) string { + if replacement, ok := replacements[match]; ok { + return replacement + } + // 如果找不到对应的替换内容,保留原占位符 + return match + }) + + return result } diff --git a/internal/model/message.go b/internal/model/message.go index 390530c..ae61c59 100644 --- a/internal/model/message.go +++ b/internal/model/message.go @@ -1,197 +1,215 @@ package model import ( - "path/filepath" + "encoding/xml" + "fmt" "strings" "time" - "github.com/sjzar/chatlog/internal/model/wxproto" - "github.com/sjzar/chatlog/pkg/util/lz4" - - "google.golang.org/protobuf/proto" + "github.com/sjzar/chatlog/pkg/util" ) +var Debug = false + const ( - // Source WeChatV3 = "wechatv3" WeChatV4 = "wechatv4" WeChatDarwinV3 = "wechatdarwinv3" ) 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 int64 `json:"type"` // 消息类型 - SubType int `json:"subType"` // 消息子类型 - Content string `json:"content"` // 消息内容,文字聊天内容 或 XML - CompressContent []byte `json:"compressContent"` // 非文字聊天内容,如图片、语音、视频等 - IsChatRoom bool `json:"isChatRoom"` // 是否为群聊消息 - ChatRoomSender string `json:"chatRoomSender"` // 群聊消息发送人 + Version string `json:"-"` // 消息版本,内部判断 + Seq int64 `json:"seq"` // 消息序号,10位时间戳 + 3位序号 + Time time.Time `json:"time"` // 消息创建时间,10位时间戳 + Talker string `json:"talker"` // 聊天对象,微信 ID or 群 ID + TalkerName string `json:"talkerName"` // 聊天对象名称 + IsChatRoom bool `json:"isChatRoom"` // 是否为群聊消息 + Sender string `json:"sender"` // 发送人,微信 ID + SenderName string `json:"senderName"` // 发送人名称 + IsSelf bool `json:"isSelf"` // 是否为自己发送的消息 + Type int64 `json:"type"` // 消息类型 + SubType int64 `json:"subType"` // 消息子类型 + Content string `json:"content"` // 消息内容,文字聊天内容 + Contents map[string]interface{} `json:"contents,omitempty"` // 消息内容,多媒体消息,采用更灵活的记录方式 - // Fill Info - // 从联系人等信息中填充 - DisplayName string `json:"-"` // 显示名称 - ChatRoomName string `json:"-"` // 群聊名称 - MediaMessage *MediaMessage `json:"-"` // 多媒体消息 - - Version string `json:"-"` // 消息版本,内部判断 + // Debug Info + MediaMsg *MediaMsg `json:"mediaMsg,omitempty"` // 原始多媒体消息,XML 格式 + SysMsg *SysMsg `json:"sysMsg,omitempty"` // 原始系统消息,XML 格式 } -// 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 int64 `json:"Type"` // 消息类型 - SubType int `json:"SubType"` // 消息子类型 - StrContent string `json:"StrContent"` // 消息内容,文字聊天内容 或 XML - CompressContent []byte `json:"CompressContent"` // 非文字聊天内容,如图片、语音、视频等 - BytesExtra []byte `json:"BytesExtra"` // protobuf 额外数据,记录群聊发送人等信息 +func (m *Message) ParseMediaInfo(data string) error { - // 非关键信息,后续有需要再加入 - // 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"` -} + m.Type, m.SubType = util.SplitInt64ToTwoInt32(m.Type) -func (m *MessageV3) Wrap() *Message { - - _m := &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, - Version: WeChatV3, - } - - _m.IsChatRoom = strings.HasSuffix(_m.Talker, "@chatroom") - - if _m.Type == 49 { - b, err := lz4.Decompress(m.CompressContent) - if err == nil { - _m.Content = string(b) - } - } - - if _m.Type != 1 { - mediaMessage, err := NewMediaMessage(_m.Type, _m.Content) - if err == nil { - _m.MediaMessage = mediaMessage - } - } - - if len(m.BytesExtra) != 0 { - if bytesExtra := ParseBytesExtra(m.BytesExtra); bytesExtra != nil { - if _m.IsChatRoom { - _m.ChatRoomSender = bytesExtra[1] - } - // FIXME xml 中的 md5 数据无法匹配到 hardlink 记录,所以直接用 proto 数据 - if _m.Type == 43 { - path := bytesExtra[4] - parts := strings.Split(filepath.ToSlash(path), "/") - if len(parts) > 1 { - path = strings.Join(parts[1:], "/") - } - _m.MediaMessage.MediaPath = path - } - } - } - - return _m -} - -// ParseBytesExtra 解析额外数据 -// 按需解析 -func ParseBytesExtra(b []byte) map[int]string { - var pbMsg wxproto.BytesExtra - if err := proto.Unmarshal(b, &pbMsg); err != nil { - return nil - } - if pbMsg.Items == nil { + if m.Type == 1 { + m.Content = data return nil } - ret := make(map[int]string, len(pbMsg.Items)) - for _, item := range pbMsg.Items { - ret[int(item.Type)] = item.Value + if m.Type == 10000 { + var sysMsg SysMsg + if err := xml.Unmarshal([]byte(data), &sysMsg); err != nil { + m.Content = data + return nil + } + if Debug { + m.SysMsg = &sysMsg + } + m.Content = sysMsg.String() + return nil } - return ret + var msg MediaMsg + err := xml.Unmarshal([]byte(data), &msg) + if err != nil { + return err + } + + if m.Contents == nil { + m.Contents = make(map[string]interface{}) + } + + if Debug { + m.MediaMsg = &msg + } + + switch m.Type { + case 3: + m.Contents["md5"] = msg.Image.MD5 + case 43: + m.Contents["md5"] = msg.Video.RawMd5 + case 49: + m.SubType = int64(msg.App.Type) + switch m.SubType { + case 5: + // 链接 + m.Contents["title"] = msg.App.Title + m.Contents["url"] = msg.App.URL + case 6: + // 文件 + m.Contents["title"] = msg.App.Title + m.Contents["md5"] = msg.App.MD5 + case 19: + // 合并转发 + m.Contents["title"] = msg.App.Title + m.Contents["desc"] = msg.App.Des + if msg.App.RecordItem == nil { + break + } + recordInfo := &RecordInfo{} + err := xml.Unmarshal([]byte(msg.App.RecordItem.CDATA), recordInfo) + if err != nil { + return err + } + m.Contents["recordInfo"] = recordInfo + case 33, 36: + // 小程序 + m.Contents["title"] = msg.App.SourceDisplayName + m.Contents["url"] = msg.App.URL + case 51: + // 视频号 + if msg.App.FinderFeed == nil { + break + } + m.Contents["title"] = msg.App.FinderFeed.Desc + if len(msg.App.FinderFeed.MediaList.Media) > 0 { + m.Contents["url"] = msg.App.FinderFeed.MediaList.Media[0].URL + } + case 57: + // 引用 + m.Content = msg.App.Title + if msg.App.ReferMsg == nil { + break + } + subMsg := &Message{ + Type: int64(msg.App.ReferMsg.Type), + Time: time.Unix(msg.App.ReferMsg.CreateTime, 0), + Sender: msg.App.ReferMsg.ChatUsr, + SenderName: msg.App.ReferMsg.DisplayName, + } + if subMsg.Sender == "" { + subMsg.Sender = msg.App.ReferMsg.FromUsr + } + if err := subMsg.ParseMediaInfo(msg.App.ReferMsg.Content); err != nil { + break + } + m.Contents["refer"] = subMsg + case 62: + // 拍一拍 + if msg.App.PatMsg == nil { + break + } + if len(msg.App.PatMsg.Records.Record) == 0 { + break + } + m.Sender = msg.App.PatMsg.Records.Record[0].FromUser + m.Content = msg.App.PatMsg.Records.Record[0].Templete + case 2000: + // 微信转账 + if msg.App.WCPayInfo == nil { + break + } + // 1 实时转账 + // 3 实时转账收钱回执 + // 4 转账退还回执 + // 5 非实时转账收钱回执 + // 7 非实时转账 + _type := "" + switch msg.App.WCPayInfo.PaySubType { + case 1, 7: + _type = "发送 " + case 3, 5: + _type = "接收 " + case 4: + _type = "退还 " + } + payMemo := "" + if len(msg.App.WCPayInfo.PayMemo) > 0 { + payMemo = "(" + msg.App.WCPayInfo.PayMemo + ")" + } + m.Content = fmt.Sprintf("[转账|%s%s]%s", _type, msg.App.WCPayInfo.FeeDesc, payMemo) + } + } + + return nil +} + +func (m *Message) SetContent(key string, value interface{}) { + if m.Contents == nil { + m.Contents = make(map[string]interface{}) + } + m.Contents[key] = value } func (m *Message) PlainText(showChatRoom bool, host string) string { + + m.SetContent("host", host) + buf := strings.Builder{} - talker := m.Talker - if m.IsSender == 1 { - talker = "我" - } else if m.IsChatRoom { - talker = m.ChatRoomSender + sender := m.Sender + switch { + case m.Type == 10000: + sender = "系统消息" + case m.IsSelf: + sender = "我" + default: + sender = m.Sender } - if m.DisplayName != "" { - buf.WriteString(m.DisplayName) + if m.SenderName != "" { + buf.WriteString(m.SenderName) buf.WriteString("(") - buf.WriteString(talker) + buf.WriteString(sender) buf.WriteString(")") } else { - buf.WriteString(talker) + buf.WriteString(sender) } buf.WriteString(" ") if m.IsChatRoom && showChatRoom { buf.WriteString("[") - if m.ChatRoomName != "" { - buf.WriteString(m.ChatRoomName) + if m.TalkerName != "" { + buf.WriteString(m.TalkerName) buf.WriteString("(") buf.WriteString(m.Talker) buf.WriteString(")") @@ -201,17 +219,112 @@ func (m *Message) PlainText(showChatRoom bool, host string) string { buf.WriteString("] ") } - buf.WriteString(m.CreateTime.Format("2006-01-02 15:04:05")) + buf.WriteString(m.Time.Format("2006-01-02 15:04:05")) buf.WriteString("\n") - if m.MediaMessage != nil { - m.MediaMessage.SetHost(host) - buf.WriteString(m.MediaMessage.String()) - } else { - buf.WriteString(m.Content) - } - + buf.WriteString(m.PlainTextContent()) buf.WriteString("\n") return buf.String() } + +func (m *Message) PlainTextContent() string { + switch m.Type { + case 1: + return m.Content + case 3: + return fmt.Sprintf("![图片](http://%s/image/%s)", m.Contents["host"], m.Contents["md5"]) + case 34: + return "[语音]" + case 42: + return "[名片]" + case 43: + if path, ok := m.Contents["path"]; ok { + return fmt.Sprintf("![视频](http://%s/data/%s)", m.Contents["host"], path) + } + return fmt.Sprintf("![视频](http://%s/video/%s)", m.Contents["host"], m.Contents["md5"]) + case 47: + return "[动画表情]" + case 49: + switch m.SubType { + case 5: + return fmt.Sprintf("[链接|%s](%s)", m.Contents["title"], m.Contents["url"]) + case 6: + return fmt.Sprintf("[文件|%s](http://%s/file/%s)", m.Contents["title"], m.Contents["host"], m.Contents["md5"]) + case 8: + return "[GIF表情]" + case 19: + _recordInfo, ok := m.Contents["recordInfo"] + if !ok { + return "[合并转发]" + } + recordInfo, ok := _recordInfo.(*RecordInfo) + if !ok { + return "[合并转发]" + } + return recordInfo.String("", m.Contents["host"].(string)) + case 33, 36: + if m.Contents["title"] == "" { + return "[小程序]" + } + return fmt.Sprintf("[小程序|%s](%s)", m.Contents["title"], m.Contents["url"]) + case 51: + if m.Contents["title"] == "" { + return "[视频号]" + } else { + return fmt.Sprintf("[视频号|%s](%s)", m.Contents["title"], m.Contents["url"]) + } + case 57: + _refer, ok := m.Contents["refer"] + if !ok { + if m.Content == "" { + return "[引用]" + } + return "> [引用]\n" + m.Content + } + refer, ok := _refer.(*Message) + if !ok { + if m.Content == "" { + return "[引用]" + } + return "> [引用]\n" + m.Content + } + buf := strings.Builder{} + referContent := refer.PlainText(false, m.Contents["host"].(string)) + for _, line := range strings.Split(referContent, "\n") { + if line == "" { + continue + } + buf.WriteString("> ") + buf.WriteString(line) + buf.WriteString("\n") + } + buf.WriteString(m.Content) + return buf.String() + case 62: + return m.Content + case 63: + return "[视频号]" + case 87: + return "[群公告]" + case 2000: + return m.Content + case 2001: + return "[红包]" + case 2003: + return "[红包封面]" + default: + return "[分享]" + } + case 50: + return "[语音通话]" + case 10000: + return m.Content + default: + content := m.Content + if len(content) > 120 { + content = content[:120] + "<...>" + } + return fmt.Sprintf("Type: %d Content: %s", m.Type, content) + } +} diff --git a/internal/model/message_darwinv3.go b/internal/model/message_darwinv3.go index 48dfbf7..fe73554 100644 --- a/internal/model/message_darwinv3.go +++ b/internal/model/message_darwinv3.go @@ -27,48 +27,31 @@ type MessageDarwinV3 struct { MsgContent string `json:"msgContent"` MessageType int64 `json:"messageType"` MesDes int `json:"mesDes"` // 0: 发送, 1: 接收 - - // MesLocalID int64 `json:"mesLocalID"` - // MesSvrID int64 `json:"mesSvrID"` - // MesStatus int `json:"mesStatus"` - // MesImgStatus int `json:"mesImgStatus"` - // MsgSource string `json:"msgSource"` - // IntRes1 int `json:"IntRes1"` - // IntRes2 int `json:"IntRes2"` - // StrRes1 string `json:"StrRes1"` - // StrRes2 string `json:"StrRes2"` - // MesVoiceText string `json:"mesVoiceText"` - // MesSeq int `json:"mesSeq"` - // CompressContent []byte `json:"CompressContent"` - // ConBlob []byte `json:"ConBlob"` } func (m *MessageDarwinV3) Wrap(talker string) *Message { _m := &Message{ - CreateTime: time.Unix(m.MsgCreateTime, 0), + Time: time.Unix(m.MsgCreateTime, 0), Type: m.MessageType, - IsSender: (m.MesDes + 1) % 2, + Talker: talker, + IsChatRoom: strings.HasSuffix(talker, "@chatroom"), + IsSelf: m.MesDes == 0, Version: WeChatDarwinV3, } - _m.IsChatRoom = strings.HasSuffix(talker, "@chatroom") - - _m.Content = m.MsgContent + content := m.MsgContent if _m.IsChatRoom { - split := strings.SplitN(m.MsgContent, ":\n", 2) + split := strings.SplitN(content, ":\n", 2) if len(split) == 2 { - _m.ChatRoomSender = split[0] - _m.Content = split[1] + _m.Sender = split[0] + content = split[1] } + } else if !_m.IsSelf { + _m.Sender = talker } - if _m.Type != 1 { - mediaMessage, err := NewMediaMessage(_m.Type, _m.Content) - if err == nil { - _m.MediaMessage = mediaMessage - } - } + _m.ParseMediaInfo(content) return _m } diff --git a/internal/model/message_v3.go b/internal/model/message_v3.go new file mode 100644 index 0000000..c57b351 --- /dev/null +++ b/internal/model/message_v3.go @@ -0,0 +1,117 @@ +package model + +import ( + "path/filepath" + "strings" + "time" + + "github.com/sjzar/chatlog/internal/model/wxproto" + "github.com/sjzar/chatlog/pkg/util/lz4" + "google.golang.org/protobuf/proto" +) + +// 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位时间戳 + StrTalker string `json:"StrTalker"` // 聊天对象,微信 ID or 群 ID + IsSender int `json:"IsSender"` // 是否为发送消息,0 接收消息,1 发送消息 + Type int64 `json:"Type"` // 消息类型 + SubType int `json:"SubType"` // 消息子类型 + StrContent string `json:"StrContent"` // 消息内容,文字聊天内容 或 XML + CompressContent []byte `json:"CompressContent"` // 非文字聊天内容,如图片、语音、视频等 + BytesExtra []byte `json:"BytesExtra"` // protobuf 额外数据,记录群聊发送人等信息 +} + +func (m *MessageV3) Wrap() *Message { + + _m := &Message{ + Seq: m.Sequence, + Time: time.Unix(m.CreateTime, 0), + Talker: m.StrTalker, + IsChatRoom: strings.HasSuffix(m.StrTalker, "@chatroom"), + IsSelf: m.IsSender == 1, + Type: m.Type, + SubType: int64(m.SubType), + Content: m.StrContent, + Version: WeChatV3, + } + + if !_m.IsChatRoom && !_m.IsSelf { + _m.Sender = m.StrTalker + } + + if _m.Type == 49 { + b, err := lz4.Decompress(m.CompressContent) + if err == nil { + _m.Content = string(b) + } + } + + _m.ParseMediaInfo(_m.Content) + + if len(m.BytesExtra) != 0 { + if bytesExtra := ParseBytesExtra(m.BytesExtra); bytesExtra != nil { + if _m.IsChatRoom { + _m.Sender = bytesExtra[1] + } + // FIXME xml 中的 md5 数据无法匹配到 hardlink 记录,所以直接用 proto 数据 + if _m.Type == 43 { + path := bytesExtra[4] + parts := strings.Split(filepath.ToSlash(path), "/") + if len(parts) > 1 { + path = strings.Join(parts[1:], "/") + } + _m.Contents["path"] = path + } + } + } + + return _m +} + +// ParseBytesExtra 解析额外数据 +// 按需解析 +func ParseBytesExtra(b []byte) map[int]string { + var pbMsg wxproto.BytesExtra + if err := proto.Unmarshal(b, &pbMsg); err != nil { + return nil + } + if pbMsg.Items == nil { + return nil + } + + ret := make(map[int]string, len(pbMsg.Items)) + for _, item := range pbMsg.Items { + ret[int(item.Type)] = item.Value + } + + return ret +} diff --git a/internal/model/message_v4.go b/internal/model/message_v4.go index 88987ba..efc80fd 100644 --- a/internal/model/message_v4.go +++ b/internal/model/message_v4.go @@ -32,75 +32,56 @@ import ( type MessageV4 struct { SortSeq int64 `json:"sort_seq"` // 消息序号,10位时间戳 + 3位序号 LocalType int64 `json:"local_type"` // 消息类型 - RealSenderID int `json:"real_sender_id"` // 发送人 ID,对应 Name2Id 表序号 + UserName string `json:"user_name"` // 发送人,通过 Join 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"` + Status int `json:"status"` // 消息状态,2 是已发送,4 是已接收,可以用于判断 IsSender(FIXME 不准, 需要判断 UserName) } -func (m *MessageV4) Wrap(id2Name map[int]string, isChatRoom bool) *Message { +func (m *MessageV4) Wrap(talker string) *Message { _m := &Message{ - Sequence: m.SortSeq, - CreateTime: time.Unix(m.CreateTime, 0), - TalkerID: m.RealSenderID, // 依赖 Name2Id 表进行转换为 StrTalker + Seq: m.SortSeq, + Time: time.Unix(m.CreateTime, 0), + Talker: talker, + IsChatRoom: strings.HasSuffix(talker, "@chatroom"), + Sender: m.UserName, Type: m.LocalType, + Contents: make(map[string]interface{}), Version: WeChatV4, } - if name, ok := id2Name[m.RealSenderID]; ok { - _m.Talker = name - } - - if m.Status == 2 { - _m.IsSender = 1 - } + // FIXME 后续通过 UserName 判断是否是自己发送的消息,目前可能不准确 + _m.IsSelf = m.Status == 2 || (!_m.IsChatRoom && talker != m.UserName) + content := "" if bytes.HasPrefix(m.MessageContent, []byte{0x28, 0xb5, 0x2f, 0xfd}) { if b, err := zstd.Decompress(m.MessageContent); err == nil { - _m.Content = string(b) + content = string(b) } } else { - _m.Content = string(m.MessageContent) + content = string(m.MessageContent) } - if isChatRoom { - _m.IsChatRoom = true - split := strings.SplitN(_m.Content, ":\n", 2) + if _m.IsChatRoom { + split := strings.SplitN(content, ":\n", 2) if len(split) == 2 { - _m.ChatRoomSender = split[0] - _m.Content = split[1] + _m.Sender = split[0] + content = split[1] } } - if _m.Type != 1 { - mediaMessage, err := NewMediaMessage(_m.Type, _m.Content) - if err == nil { - _m.MediaMessage = mediaMessage - _m.Type = mediaMessage.Type - _m.SubType = mediaMessage.SubType - } - } + _m.ParseMediaInfo(content) if len(m.PackedInfoData) != 0 { if packedInfo := ParsePackedInfo(m.PackedInfoData); packedInfo != nil { // FIXME 尝试解决 v4 版本 xml 数据无法匹配到 hardlink 记录的问题 if _m.Type == 3 && packedInfo.Image != nil { - _m.MediaMessage.MediaMD5 = packedInfo.Image.Md5 + _m.Contents["md5"] = packedInfo.Image.Md5 } if _m.Type == 43 && packedInfo.Video != nil { - _m.MediaMessage.MediaMD5 = packedInfo.Video.Md5 + _m.Contents["md5"] = packedInfo.Video.Md5 } } } diff --git a/internal/wechatdb/datasource/v4/datasource.go b/internal/wechatdb/datasource/v4/datasource.go index dfd2bb3..b45206a 100644 --- a/internal/wechatdb/datasource/v4/datasource.go +++ b/internal/wechatdb/datasource/v4/datasource.go @@ -30,7 +30,6 @@ type MessageDBInfo struct { FilePath string StartTime time.Time EndTime time.Time - ID2Name map[int]string } type DataSource struct { @@ -99,32 +98,10 @@ func (ds *DataSource) initMessageDbs(path string) error { } startTime = time.Unix(timestamp, 0) - // 获取 ID2Name 映射 - id2Name := make(map[int]string) - rows, err := db.Query("SELECT user_name FROM Name2Id") - if err != nil { - log.Err(err).Msgf("获取数据库 %s 的 Name2Id 表失败", filePath) - db.Close() - continue - } - - i := 1 - for rows.Next() { - var name string - if err := rows.Scan(&name); err != nil { - log.Err(err).Msgf("数据库 %s 扫描 Name2Id 行失败", filePath) - continue - } - id2Name[i] = name - i++ - } - rows.Close() - // 保存数据库信息 ds.messageFiles = append(ds.messageFiles, MessageDBInfo{ FilePath: filePath, StartTime: startTime, - ID2Name: id2Name, }) // 保存数据库连接 @@ -253,7 +230,7 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T // 对所有消息按时间排序 sort.Slice(totalMessages, func(i, j int) bool { - return totalMessages[i].Sequence < totalMessages[j].Sequence + return totalMessages[i].Seq < totalMessages[j].Seq }) // 处理分页 @@ -288,10 +265,11 @@ func (ds *DataSource) getMessagesSingleFile(ctx context.Context, dbInfo MessageD args := []interface{}{startTime.Unix(), endTime.Unix()} query := fmt.Sprintf(` - SELECT sort_seq, local_type, real_sender_id, create_time, message_content, packed_info_data, status - FROM %s + SELECT m.sort_seq, m.local_type, n.user_name, m.create_time, m.message_content, m.packed_info_data, m.status + FROM %s m + LEFT JOIN Name2Id n ON m.real_sender_id = n.rowid WHERE %s - ORDER BY sort_seq ASC + ORDER BY m.sort_seq ASC `, tableName, strings.Join(conditions, " AND ")) if limit > 0 { @@ -310,14 +288,13 @@ func (ds *DataSource) getMessagesSingleFile(ctx context.Context, dbInfo MessageD // 处理查询结果 messages := []*model.Message{} - isChatRoom := strings.HasSuffix(talker, "@chatroom") for rows.Next() { var msg model.MessageV4 err := rows.Scan( &msg.SortSeq, &msg.LocalType, - &msg.RealSenderID, + &msg.UserName, &msg.CreateTime, &msg.MessageContent, &msg.PackedInfoData, @@ -327,7 +304,7 @@ func (ds *DataSource) getMessagesSingleFile(ctx context.Context, dbInfo MessageD return nil, errors.ScanRowFailed(err) } - messages = append(messages, msg.Wrap(dbInfo.ID2Name, isChatRoom)) + messages = append(messages, msg.Wrap(talker)) } return messages, nil @@ -359,10 +336,11 @@ func (ds *DataSource) getMessagesFromDB(ctx context.Context, db *sql.DB, dbInfo args := []interface{}{startTime.Unix(), endTime.Unix()} query := fmt.Sprintf(` - SELECT sort_seq, local_type, real_sender_id, create_time, message_content, packed_info_data, status - FROM %s + SELECT m.sort_seq, m.local_type, n.user_name, m.create_time, m.message_content, m.packed_info_data, m.status + FROM %s m + LEFT JOIN Name2Id n ON m.real_sender_id = n.rowid WHERE %s - ORDER BY sort_seq ASC + ORDER BY m.sort_seq ASC `, tableName, strings.Join(conditions, " AND ")) // 执行查询 @@ -378,14 +356,13 @@ func (ds *DataSource) getMessagesFromDB(ctx context.Context, db *sql.DB, dbInfo // 处理查询结果 messages := []*model.Message{} - isChatRoom := strings.HasSuffix(talker, "@chatroom") for rows.Next() { var msg model.MessageV4 err := rows.Scan( &msg.SortSeq, &msg.LocalType, - &msg.RealSenderID, + &msg.UserName, &msg.CreateTime, &msg.MessageContent, &msg.PackedInfoData, @@ -395,7 +372,7 @@ func (ds *DataSource) getMessagesFromDB(ctx context.Context, db *sql.DB, dbInfo return nil, errors.ScanRowFailed(err) } - messages = append(messages, msg.Wrap(dbInfo.ID2Name, isChatRoom)) + messages = append(messages, msg.Wrap(talker)) } return messages, nil diff --git a/internal/wechatdb/datasource/windowsv3/datasource.go b/internal/wechatdb/datasource/windowsv3/datasource.go index a74dd05..8068e9b 100644 --- a/internal/wechatdb/datasource/windowsv3/datasource.go +++ b/internal/wechatdb/datasource/windowsv3/datasource.go @@ -293,7 +293,7 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T } query := fmt.Sprintf(` - SELECT Sequence, CreateTime, TalkerId, StrTalker, IsSender, + SELECT Sequence, CreateTime, StrTalker, IsSender, Type, SubType, StrContent, CompressContent, BytesExtra FROM MSG WHERE %s @@ -316,7 +316,6 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T err := rows.Scan( &msg.Sequence, &msg.CreateTime, - &msg.TalkerID, &msg.StrTalker, &msg.IsSender, &msg.Type, @@ -343,7 +342,7 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T // 对所有消息按时间排序 sort.Slice(totalMessages, func(i, j int) bool { - return totalMessages[i].Sequence < totalMessages[j].Sequence + return totalMessages[i].Seq < totalMessages[j].Seq }) // 处理分页 @@ -378,7 +377,7 @@ func (ds *DataSource) getMessagesSingleFile(ctx context.Context, dbInfo MessageD } } query := fmt.Sprintf(` - SELECT Sequence, CreateTime, TalkerId, StrTalker, IsSender, + SELECT Sequence, CreateTime, StrTalker, IsSender, Type, SubType, StrContent, CompressContent, BytesExtra FROM MSG WHERE %s @@ -409,7 +408,6 @@ func (ds *DataSource) getMessagesSingleFile(ctx context.Context, dbInfo MessageD err := rows.Scan( &msg.Sequence, &msg.CreateTime, - &msg.TalkerID, &msg.StrTalker, &msg.IsSender, &msg.Type, diff --git a/internal/wechatdb/repository/message.go b/internal/wechatdb/repository/message.go index 28e60d3..199142d 100644 --- a/internal/wechatdb/repository/message.go +++ b/internal/wechatdb/repository/message.go @@ -41,28 +41,24 @@ func (r *Repository) EnrichMessages(ctx context.Context, messages []*model.Messa // enrichMessage 补充单条消息的额外信息 func (r *Repository) enrichMessage(msg *model.Message) { - talker := msg.Talker - // 处理群聊消息 if msg.IsChatRoom { - talker = msg.ChatRoomSender - // 补充群聊名称 if chatRoom, ok := r.chatRoomCache[msg.Talker]; ok { - msg.ChatRoomName = chatRoom.DisplayName() + msg.TalkerName = chatRoom.DisplayName() // 补充发送者在群里的显示名称 - if displayName, ok := chatRoom.User2DisplayName[talker]; ok { - msg.DisplayName = displayName + if displayName, ok := chatRoom.User2DisplayName[msg.Sender]; ok { + msg.SenderName = displayName } } } // 如果不是自己发送的消息且还没有显示名称,尝试补充发送者信息 - if msg.DisplayName == "" && msg.IsSender != 1 { - contact := r.getFullContact(talker) + if msg.SenderName == "" && !msg.IsSelf { + contact := r.getFullContact(msg.Sender) if contact != nil { - msg.DisplayName = contact.DisplayName() + msg.SenderName = contact.DisplayName() } } }