Compare commits
2 Commits
v0.0.12
...
feature/ms
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d92e974ce6 | ||
|
|
4250057ba8 |
@@ -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"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -94,6 +94,7 @@ type Tool struct {
|
||||
type ToolSchema struct {
|
||||
Type string `json:"type"`
|
||||
Properties M `json:"properties"`
|
||||
Required []string `json:"required,omitempty"`
|
||||
}
|
||||
|
||||
// {
|
||||
|
||||
@@ -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("", m.Host, m.MediaMD5)
|
||||
case 34:
|
||||
return "[语音]"
|
||||
case 43:
|
||||
if m.MediaPath != "" {
|
||||
return fmt.Sprintf("", m.Host, m.MediaPath)
|
||||
}
|
||||
return fmt.Sprintf("", 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("", 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"`
|
||||
@@ -267,10 +62,14 @@ type App struct {
|
||||
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 文件
|
||||
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(" \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
|
||||
}
|
||||
|
||||
@@ -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"` // 群聊消息发送人
|
||||
|
||||
// Fill Info
|
||||
// 从联系人等信息中填充
|
||||
DisplayName string `json:"-"` // 显示名称
|
||||
ChatRoomName string `json:"-"` // 群聊名称
|
||||
MediaMessage *MediaMessage `json:"-"` // 多媒体消息
|
||||
|
||||
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"` // 消息内容,多媒体消息,采用更灵活的记录方式
|
||||
|
||||
// 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("", m.Contents["host"], m.Contents["md5"])
|
||||
case 34:
|
||||
return "[语音]"
|
||||
case 42:
|
||||
return "[名片]"
|
||||
case 43:
|
||||
if path, ok := m.Contents["path"]; ok {
|
||||
return fmt.Sprintf("", m.Contents["host"], path)
|
||||
}
|
||||
return fmt.Sprintf("", 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
117
internal/model/message_v3.go
Normal file
117
internal/model/message_v3.go
Normal file
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user