Support Media Message (#9)
This commit is contained in:
44
internal/model/media.go
Normal file
44
internal/model/media.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Media struct {
|
||||
Type string `json:"type"` // 媒体类型:image, video, voice, file
|
||||
Key string `json:"key"` // MD5
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
ModifyTime int64 `json:"modifyTime"`
|
||||
}
|
||||
|
||||
type MediaV3 struct {
|
||||
Type string `json:"type"`
|
||||
Key string `json:"key"`
|
||||
Dir1 string `json:"dir1"`
|
||||
Dir2 string `json:"dir2"`
|
||||
Name string `json:"name"`
|
||||
ModifyTime int64 `json:"modifyTime"`
|
||||
}
|
||||
|
||||
func (m *MediaV3) Wrap() *Media {
|
||||
|
||||
var path string
|
||||
switch m.Type {
|
||||
case "image":
|
||||
path = filepath.Join("FileStorage", "MsgAttach", m.Dir1, "Image", m.Dir2, m.Name)
|
||||
case "video":
|
||||
path = filepath.Join("FileStorage", "Video", m.Dir2, m.Name)
|
||||
case "file":
|
||||
path = filepath.Join("FileStorage", "File", m.Dir2, m.Name)
|
||||
}
|
||||
|
||||
return &Media{
|
||||
Type: m.Type,
|
||||
Key: m.Key,
|
||||
ModifyTime: m.ModifyTime,
|
||||
Path: path,
|
||||
Name: m.Name,
|
||||
}
|
||||
}
|
||||
40
internal/model/media_darwinv3.go
Normal file
40
internal/model/media_darwinv3.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package model
|
||||
|
||||
import "path/filepath"
|
||||
|
||||
// CREATE TABLE HlinkMediaRecord(
|
||||
// mediaMd5 TEXT,
|
||||
// mediaSize INTEGER,
|
||||
// inodeNumber INTEGER,
|
||||
// modifyTime INTEGER ,
|
||||
// CONSTRAINT _Md5_Size UNIQUE (mediaMd5,mediaSize)
|
||||
// )
|
||||
// CREATE TABLE HlinkMediaDetail(
|
||||
// localId INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
// inodeNumber INTEGER,
|
||||
// relativePath TEXT,
|
||||
// fileName TEXT
|
||||
// )
|
||||
type MediaDarwinV3 struct {
|
||||
MediaMd5 string `json:"mediaMd5"`
|
||||
MediaSize int64 `json:"mediaSize"`
|
||||
InodeNumber int64 `json:"inodeNumber"`
|
||||
ModifyTime int64 `json:"modifyTime"`
|
||||
RelativePath string `json:"relativePath"`
|
||||
FileName string `json:"fileName"`
|
||||
}
|
||||
|
||||
func (m *MediaDarwinV3) Wrap() *Media {
|
||||
|
||||
path := filepath.Join("Message/MessageTemp", m.RelativePath, m.FileName)
|
||||
name := filepath.Base(path)
|
||||
|
||||
return &Media{
|
||||
Type: "",
|
||||
Key: m.MediaMd5,
|
||||
Size: m.MediaSize,
|
||||
ModifyTime: m.ModifyTime,
|
||||
Path: path,
|
||||
Name: name,
|
||||
}
|
||||
}
|
||||
35
internal/model/media_v4.go
Normal file
35
internal/model/media_v4.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package model
|
||||
|
||||
import "path/filepath"
|
||||
|
||||
type MediaV4 struct {
|
||||
Type string `json:"type"`
|
||||
Key string `json:"key"`
|
||||
Dir1 string `json:"dir1"`
|
||||
Dir2 string `json:"dir2"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
ModifyTime int64 `json:"modifyTime"`
|
||||
}
|
||||
|
||||
func (m *MediaV4) Wrap() *Media {
|
||||
|
||||
var path string
|
||||
switch m.Type {
|
||||
case "image":
|
||||
path = filepath.Join("msg", "attach", m.Dir1, m.Dir2, "Img", m.Name)
|
||||
case "video":
|
||||
path = filepath.Join("msg", "video", m.Dir1, m.Name)
|
||||
case "file":
|
||||
path = filepath.Join("msg", "file", m.Dir1, m.Name)
|
||||
}
|
||||
|
||||
return &Media{
|
||||
Type: m.Type,
|
||||
Key: m.Key,
|
||||
Path: path,
|
||||
Name: m.Name,
|
||||
Size: m.Size,
|
||||
ModifyTime: m.ModifyTime,
|
||||
}
|
||||
}
|
||||
355
internal/model/mediamessage.go
Normal file
355
internal/model/mediamessage.go
Normal file
@@ -0,0 +1,355 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"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 {
|
||||
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"`
|
||||
// Length string `xml:"length,attr"`
|
||||
// AesKey string `xml:"aeskey,attr"`
|
||||
// EncryVer string `xml:"encryver,attr"`
|
||||
// OriginSourceMd5 string `xml:"originsourcemd5,attr"`
|
||||
// FileKey string `xml:"filekey,attr"`
|
||||
// UploadContinueCount string `xml:"uploadcontinuecount,attr"`
|
||||
// ImgSourceUrl string `xml:"imgsourceurl,attr"`
|
||||
// HevcMidSize string `xml:"hevc_mid_size,attr"`
|
||||
// CdnBigImgUrl string `xml:"cdnbigimgurl,attr"`
|
||||
// CdnMidImgUrl string `xml:"cdnmidimgurl,attr"`
|
||||
// CdnThumbUrl string `xml:"cdnthumburl,attr"`
|
||||
// CdnThumbLength string `xml:"cdnthumblength,attr"`
|
||||
// CdnThumbWidth string `xml:"cdnthumbwidth,attr"`
|
||||
// CdnThumbHeight string `xml:"cdnthumbheight,attr"`
|
||||
// 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"`
|
||||
// PlayLength string `xml:"playlength,attr"`
|
||||
// Offset string `xml:"offset,attr"`
|
||||
// FromUserName string `xml:"fromusername,attr"`
|
||||
// Status string `xml:"status,attr"`
|
||||
// Compress string `xml:"compress,attr"`
|
||||
// CameraType string `xml:"cameratype,attr"`
|
||||
// Source string `xml:"source,attr"`
|
||||
// AesKey string `xml:"aeskey,attr"`
|
||||
// CdnVideoUrl string `xml:"cdnvideourl,attr"`
|
||||
// CdnThumbUrl string `xml:"cdnthumburl,attr"`
|
||||
// CdnThumbLength string `xml:"cdnthumblength,attr"`
|
||||
// CdnThumbWidth string `xml:"cdnthumbwidth,attr"`
|
||||
// CdnThumbHeight string `xml:"cdnthumbheight,attr"`
|
||||
// CdnThumbAesKey string `xml:"cdnthumbaeskey,attr"`
|
||||
// EncryVer string `xml:"encryver,attr"`
|
||||
// RawLength string `xml:"rawlength,attr"`
|
||||
// CdnRawVideoUrl string `xml:"cdnrawvideourl,attr"`
|
||||
// CdnRawVideoAesKey string `xml:"cdnrawvideoaeskey,attr"`
|
||||
}
|
||||
|
||||
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 引用
|
||||
}
|
||||
|
||||
// ReferMsg 表示引用消息
|
||||
type ReferMsg struct {
|
||||
Type int64 `xml:"type"`
|
||||
SvrID string `xml:"svrid"`
|
||||
FromUsr string `xml:"fromusr"`
|
||||
ChatUsr string `xml:"chatusr"`
|
||||
DisplayName string `xml:"displayname"`
|
||||
MsgSource string `xml:"msgsource"`
|
||||
Content string `xml:"content"`
|
||||
StrID string `xml:"strid"`
|
||||
CreateTime int64 `xml:"createtime"`
|
||||
}
|
||||
|
||||
// AppAttach 表示应用附件
|
||||
type AppAttach struct {
|
||||
TotalLen string `xml:"totallen"`
|
||||
AttachID string `xml:"attachid"`
|
||||
CDNAttachURL string `xml:"cdnattachurl"`
|
||||
EmoticonMD5 string `xml:"emoticonmd5"`
|
||||
AESKey string `xml:"aeskey"`
|
||||
FileExt string `xml:"fileext"`
|
||||
IsLargeFileMsg string `xml:"islargefilemsg"`
|
||||
}
|
||||
|
||||
type RecordItem struct {
|
||||
CDATA string `xml:",cdata"`
|
||||
|
||||
// 解析后的记录信息
|
||||
RecordInfo *RecordInfo
|
||||
}
|
||||
|
||||
// RecordInfo 表示聊天记录信息
|
||||
type RecordInfo struct {
|
||||
XMLName xml.Name `xml:"recordinfo"`
|
||||
FromScene string `xml:"fromscene,omitempty"`
|
||||
FavUsername string `xml:"favusername,omitempty"`
|
||||
FavCreateTime string `xml:"favcreatetime,omitempty"`
|
||||
IsChatRoom string `xml:"isChatRoom,omitempty"`
|
||||
Title string `xml:"title,omitempty"`
|
||||
Desc string `xml:"desc,omitempty"`
|
||||
Info string `xml:"info,omitempty"`
|
||||
DataList DataList `xml:"datalist,omitempty"`
|
||||
}
|
||||
|
||||
// DataList 表示数据列表
|
||||
type DataList struct {
|
||||
Count string `xml:"count,attr,omitempty"`
|
||||
DataItems []DataItem `xml:"dataitem,omitempty"`
|
||||
}
|
||||
|
||||
// DataItem 表示数据项
|
||||
type DataItem struct {
|
||||
DataType string `xml:"datatype,attr,omitempty"`
|
||||
DataID string `xml:"dataid,attr,omitempty"`
|
||||
HTMLID string `xml:"htmlid,attr,omitempty"`
|
||||
DataFmt string `xml:"datafmt,omitempty"`
|
||||
SourceName string `xml:"sourcename,omitempty"`
|
||||
SourceTime string `xml:"sourcetime,omitempty"`
|
||||
SourceHeadURL string `xml:"sourceheadurl,omitempty"`
|
||||
DataDesc string `xml:"datadesc,omitempty"`
|
||||
|
||||
// 图片特有字段
|
||||
ThumbSourcePath string `xml:"thumbsourcepath,omitempty"`
|
||||
ThumbSize string `xml:"thumbsize,omitempty"`
|
||||
CDNDataURL string `xml:"cdndataurl,omitempty"`
|
||||
CDNDataKey string `xml:"cdndatakey,omitempty"`
|
||||
CDNThumbURL string `xml:"cdnthumburl,omitempty"`
|
||||
CDNThumbKey string `xml:"cdnthumbkey,omitempty"`
|
||||
DataSourcePath string `xml:"datasourcepath,omitempty"`
|
||||
FullMD5 string `xml:"fullmd5,omitempty"`
|
||||
ThumbFullMD5 string `xml:"thumbfullmd5,omitempty"`
|
||||
ThumbHead256MD5 string `xml:"thumbhead256md5,omitempty"`
|
||||
DataSize string `xml:"datasize,omitempty"`
|
||||
CDNEncryVer string `xml:"cdnencryver,omitempty"`
|
||||
SrcChatname string `xml:"srcChatname,omitempty"`
|
||||
SrcMsgLocalID string `xml:"srcMsgLocalid,omitempty"`
|
||||
SrcMsgCreateTime string `xml:"srcMsgCreateTime,omitempty"`
|
||||
MessageUUID string `xml:"messageuuid,omitempty"`
|
||||
FromNewMsgID string `xml:"fromnewmsgid,omitempty"`
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/model/wxproto"
|
||||
"github.com/sjzar/chatlog/pkg/util/lz4"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
@@ -23,7 +24,7 @@ type Message struct {
|
||||
TalkerID int `json:"talkerID"` // 聊天对象,Name2ID 表序号,索引值
|
||||
Talker string `json:"talker"` // 聊天对象,微信 ID or 群 ID
|
||||
IsSender int `json:"isSender"` // 是否为发送消息,0 接收消息,1 发送消息
|
||||
Type int `json:"type"` // 消息类型
|
||||
Type int64 `json:"type"` // 消息类型
|
||||
SubType int `json:"subType"` // 消息子类型
|
||||
Content string `json:"content"` // 消息内容,文字聊天内容 或 XML
|
||||
CompressContent []byte `json:"compressContent"` // 非文字聊天内容,如图片、语音、视频等
|
||||
@@ -32,8 +33,9 @@ type Message struct {
|
||||
|
||||
// Fill Info
|
||||
// 从联系人等信息中填充
|
||||
DisplayName string `json:"-"` // 显示名称
|
||||
ChatRoomName string `json:"-"` // 群聊名称
|
||||
DisplayName string `json:"-"` // 显示名称
|
||||
ChatRoomName string `json:"-"` // 群聊名称
|
||||
MediaMessage *MediaMessage `json:"-"` // 多媒体消息
|
||||
|
||||
Version string `json:"-"` // 消息版本,内部判断
|
||||
}
|
||||
@@ -72,7 +74,7 @@ type MessageV3 struct {
|
||||
TalkerID int `json:"TalkerId"` // 聊天对象,Name2ID 表序号,索引值
|
||||
StrTalker string `json:"StrTalker"` // 聊天对象,微信 ID or 群 ID
|
||||
IsSender int `json:"IsSender"` // 是否为发送消息,0 接收消息,1 发送消息
|
||||
Type int `json:"Type"` // 消息类型
|
||||
Type int64 `json:"Type"` // 消息类型
|
||||
SubType int `json:"SubType"` // 消息子类型
|
||||
StrContent string `json:"StrContent"` // 消息内容,文字聊天内容 或 XML
|
||||
CompressContent []byte `json:"CompressContent"` // 非文字聊天内容,如图片、语音、视频等
|
||||
@@ -99,14 +101,7 @@ type MessageV3 struct {
|
||||
|
||||
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{
|
||||
_m := &Message{
|
||||
Sequence: m.Sequence,
|
||||
CreateTime: time.Unix(m.CreateTime, 0),
|
||||
TalkerID: m.TalkerID,
|
||||
@@ -116,33 +111,65 @@ func (m *MessageV3) Wrap() *Message {
|
||||
SubType: m.SubType,
|
||||
Content: m.StrContent,
|
||||
CompressContent: m.CompressContent,
|
||||
IsChatRoom: isChatRoom,
|
||||
ChatRoomSender: chatRoomSender,
|
||||
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) (chatRoomSender string) {
|
||||
func ParseBytesExtra(b []byte) map[int]string {
|
||||
var pbMsg wxproto.BytesExtra
|
||||
if err := proto.Unmarshal(b, &pbMsg); err != nil {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
if pbMsg.Items == nil {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
ret := make(map[int]string, len(pbMsg.Items))
|
||||
for _, item := range pbMsg.Items {
|
||||
if item.Type == 1 {
|
||||
return item.Value
|
||||
}
|
||||
ret[int(item.Type)] = item.Value
|
||||
}
|
||||
|
||||
return
|
||||
return ret
|
||||
}
|
||||
|
||||
func (m *Message) PlainText(showChatRoom bool) string {
|
||||
func (m *Message) PlainText(showChatRoom bool, host string) string {
|
||||
buf := strings.Builder{}
|
||||
|
||||
talker := m.Talker
|
||||
@@ -177,51 +204,13 @@ func (m *Message) PlainText(showChatRoom bool) string {
|
||||
buf.WriteString(m.CreateTime.Format("2006-01-02 15:04:05"))
|
||||
buf.WriteString("\n")
|
||||
|
||||
switch m.Type {
|
||||
case 1:
|
||||
if m.MediaMessage != nil {
|
||||
m.MediaMessage.SetHost(host)
|
||||
buf.WriteString(m.MediaMessage.String())
|
||||
} else {
|
||||
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:
|
||||
content := m.Content
|
||||
if len(content) > 120 {
|
||||
content = content[:120] + "<...>"
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf("Type: %d Content: %s", m.Type, content))
|
||||
}
|
||||
|
||||
buf.WriteString("\n")
|
||||
|
||||
return buf.String()
|
||||
|
||||
@@ -23,16 +23,16 @@ import (
|
||||
// ConBlob BLOB
|
||||
// )
|
||||
type MessageDarwinV3 struct {
|
||||
MesCreateTime int64 `json:"mesCreateTime"`
|
||||
MesContent string `json:"mesContent"`
|
||||
MesType int `json:"mesType"`
|
||||
MsgCreateTime int64 `json:"msgCreateTime"`
|
||||
MsgContent string `json:"msgContent"`
|
||||
MessageType int64 `json:"messageType"`
|
||||
MesDes int `json:"mesDes"` // 0: 发送, 1: 接收
|
||||
MesSource string `json:"mesSource"`
|
||||
|
||||
// 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"`
|
||||
@@ -44,26 +44,31 @@ type MessageDarwinV3 struct {
|
||||
}
|
||||
|
||||
func (m *MessageDarwinV3) Wrap(talker string) *Message {
|
||||
isChatRoom := strings.HasSuffix(talker, "@chatroom")
|
||||
|
||||
var chatRoomSender string
|
||||
content := m.MesContent
|
||||
if isChatRoom {
|
||||
split := strings.SplitN(m.MesContent, ":\n", 2)
|
||||
_m := &Message{
|
||||
CreateTime: time.Unix(m.MsgCreateTime, 0),
|
||||
Type: m.MessageType,
|
||||
IsSender: (m.MesDes + 1) % 2,
|
||||
Version: WeChatDarwinV3,
|
||||
}
|
||||
|
||||
_m.IsChatRoom = strings.HasSuffix(talker, "@chatroom")
|
||||
|
||||
_m.Content = m.MsgContent
|
||||
if _m.IsChatRoom {
|
||||
split := strings.SplitN(m.MsgContent, ":\n", 2)
|
||||
if len(split) == 2 {
|
||||
chatRoomSender = split[0]
|
||||
content = split[1]
|
||||
_m.ChatRoomSender = split[0]
|
||||
_m.Content = split[1]
|
||||
}
|
||||
}
|
||||
|
||||
return &Message{
|
||||
CreateTime: time.Unix(m.MesCreateTime, 0),
|
||||
Content: content,
|
||||
Talker: talker,
|
||||
Type: m.MesType,
|
||||
IsSender: (m.MesDes + 1) % 2,
|
||||
IsChatRoom: isChatRoom,
|
||||
ChatRoomSender: chatRoomSender,
|
||||
Version: WeChatDarwinV3,
|
||||
if _m.Type != 1 {
|
||||
mediaMessage, err := NewMediaMessage(_m.Type, _m.Content)
|
||||
if err == nil {
|
||||
_m.MediaMessage = mediaMessage
|
||||
}
|
||||
}
|
||||
|
||||
return _m
|
||||
}
|
||||
|
||||
@@ -5,7 +5,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/model/wxproto"
|
||||
"github.com/sjzar/chatlog/pkg/util/zstd"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// CREATE TABLE Msg_md5(talker)(
|
||||
@@ -29,7 +31,7 @@ import (
|
||||
// )
|
||||
type MessageV4 struct {
|
||||
SortSeq int64 `json:"sort_seq"` // 消息序号,10位时间戳 + 3位序号
|
||||
LocalType int `json:"local_type"` // 消息类型
|
||||
LocalType int64 `json:"local_type"` // 消息类型
|
||||
RealSenderID int `json:"real_sender_id"` // 发送人 ID,对应 Name2Id 表序号
|
||||
CreateTime int64 `json:"create_time"` // 消息创建时间,10位时间戳
|
||||
MessageContent []byte `json:"message_content"` // 消息内容,文字聊天内容 或 zstd 压缩内容
|
||||
@@ -50,12 +52,11 @@ type MessageV4 struct {
|
||||
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,
|
||||
Sequence: m.SortSeq,
|
||||
CreateTime: time.Unix(m.CreateTime, 0),
|
||||
TalkerID: m.RealSenderID, // 依赖 Name2Id 表进行转换为 StrTalker
|
||||
Type: m.LocalType,
|
||||
Version: WeChatV4,
|
||||
}
|
||||
|
||||
if name, ok := id2Name[m.RealSenderID]; ok {
|
||||
@@ -66,16 +67,12 @@ func (m *MessageV4) Wrap(id2Name map[int]string, isChatRoom bool) *Message {
|
||||
_m.IsSender = 1
|
||||
}
|
||||
|
||||
if _m.Type == 1 {
|
||||
_m.Content = string(m.MessageContent)
|
||||
} else {
|
||||
if bytes.HasPrefix(m.MessageContent, []byte{0x28, 0xb5, 0x2f, 0xfd}) {
|
||||
if b, err := zstd.Decompress(m.MessageContent); err == nil {
|
||||
_m.Content = string(b)
|
||||
}
|
||||
} else {
|
||||
_m.CompressContent = m.MessageContent
|
||||
if bytes.HasPrefix(m.MessageContent, []byte{0x28, 0xb5, 0x2f, 0xfd}) {
|
||||
if b, err := zstd.Decompress(m.MessageContent); err == nil {
|
||||
_m.Content = string(b)
|
||||
}
|
||||
} else {
|
||||
_m.Content = string(m.MessageContent)
|
||||
}
|
||||
|
||||
if isChatRoom {
|
||||
@@ -87,5 +84,34 @@ func (m *MessageV4) Wrap(id2Name map[int]string, isChatRoom bool) *Message {
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if _m.Type == 43 && packedInfo.Video != nil {
|
||||
_m.MediaMessage.MediaMD5 = packedInfo.Video.Md5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _m
|
||||
}
|
||||
|
||||
func ParsePackedInfo(b []byte) *wxproto.PackedInfo {
|
||||
var pbMsg wxproto.PackedInfo
|
||||
if err := proto.Unmarshal(b, &pbMsg); err != nil {
|
||||
return nil
|
||||
}
|
||||
return &pbMsg
|
||||
}
|
||||
|
||||
252
internal/model/wxproto/packedinfo.pb.go
Normal file
252
internal/model/wxproto/packedinfo.pb.go
Normal file
@@ -0,0 +1,252 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.5
|
||||
// protoc v5.29.3
|
||||
// source: packedinfo.proto
|
||||
|
||||
package wxproto
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type PackedInfo struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Type uint32 `protobuf:"varint,1,opt,name=type,proto3" json:"type,omitempty"` // 始终为 106 (0x6a)
|
||||
Version uint32 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"` // 始终为 14 (0xe)
|
||||
Image *ImageHash `protobuf:"bytes,3,opt,name=image,proto3" json:"image,omitempty"` // 图片哈希
|
||||
Video *VideoHash `protobuf:"bytes,4,opt,name=video,proto3" json:"video,omitempty"` // 视频哈希
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *PackedInfo) Reset() {
|
||||
*x = PackedInfo{}
|
||||
mi := &file_packedinfo_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *PackedInfo) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*PackedInfo) ProtoMessage() {}
|
||||
|
||||
func (x *PackedInfo) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_packedinfo_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use PackedInfo.ProtoReflect.Descriptor instead.
|
||||
func (*PackedInfo) Descriptor() ([]byte, []int) {
|
||||
return file_packedinfo_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *PackedInfo) GetType() uint32 {
|
||||
if x != nil {
|
||||
return x.Type
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *PackedInfo) GetVersion() uint32 {
|
||||
if x != nil {
|
||||
return x.Version
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *PackedInfo) GetImage() *ImageHash {
|
||||
if x != nil {
|
||||
return x.Image
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *PackedInfo) GetVideo() *VideoHash {
|
||||
if x != nil {
|
||||
return x.Video
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ImageHash struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Md5 string `protobuf:"bytes,4,opt,name=md5,proto3" json:"md5,omitempty"` // 32 字符的 MD5 哈希
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ImageHash) Reset() {
|
||||
*x = ImageHash{}
|
||||
mi := &file_packedinfo_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ImageHash) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ImageHash) ProtoMessage() {}
|
||||
|
||||
func (x *ImageHash) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_packedinfo_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ImageHash.ProtoReflect.Descriptor instead.
|
||||
func (*ImageHash) Descriptor() ([]byte, []int) {
|
||||
return file_packedinfo_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *ImageHash) GetMd5() string {
|
||||
if x != nil {
|
||||
return x.Md5
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type VideoHash struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Md5 string `protobuf:"bytes,8,opt,name=md5,proto3" json:"md5,omitempty"` // 32 字符的 MD5 哈希
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *VideoHash) Reset() {
|
||||
*x = VideoHash{}
|
||||
mi := &file_packedinfo_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *VideoHash) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*VideoHash) ProtoMessage() {}
|
||||
|
||||
func (x *VideoHash) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_packedinfo_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use VideoHash.ProtoReflect.Descriptor instead.
|
||||
func (*VideoHash) Descriptor() ([]byte, []int) {
|
||||
return file_packedinfo_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *VideoHash) GetMd5() string {
|
||||
if x != nil {
|
||||
return x.Md5
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_packedinfo_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_packedinfo_proto_rawDesc = string([]byte{
|
||||
0x0a, 0x10, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x64, 0x69, 0x6e, 0x66, 0x6f, 0x2e, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x12, 0x0c, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
|
||||
0x22, 0x98, 0x01, 0x0a, 0x0a, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x12,
|
||||
0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x74,
|
||||
0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02,
|
||||
0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2d, 0x0a,
|
||||
0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x61,
|
||||
0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6d, 0x61, 0x67,
|
||||
0x65, 0x48, 0x61, 0x73, 0x68, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x2d, 0x0a, 0x05,
|
||||
0x76, 0x69, 0x64, 0x65, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x61, 0x70,
|
||||
0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x56, 0x69, 0x64, 0x65, 0x6f,
|
||||
0x48, 0x61, 0x73, 0x68, 0x52, 0x05, 0x76, 0x69, 0x64, 0x65, 0x6f, 0x22, 0x1d, 0x0a, 0x09, 0x49,
|
||||
0x6d, 0x61, 0x67, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x64, 0x35, 0x18,
|
||||
0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x64, 0x35, 0x22, 0x1d, 0x0a, 0x09, 0x56, 0x69,
|
||||
0x64, 0x65, 0x6f, 0x48, 0x61, 0x73, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x64, 0x35, 0x18, 0x08,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x64, 0x35, 0x42, 0x0b, 0x5a, 0x09, 0x2e, 0x3b, 0x77,
|
||||
0x78, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
})
|
||||
|
||||
var (
|
||||
file_packedinfo_proto_rawDescOnce sync.Once
|
||||
file_packedinfo_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_packedinfo_proto_rawDescGZIP() []byte {
|
||||
file_packedinfo_proto_rawDescOnce.Do(func() {
|
||||
file_packedinfo_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_packedinfo_proto_rawDesc), len(file_packedinfo_proto_rawDesc)))
|
||||
})
|
||||
return file_packedinfo_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_packedinfo_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
|
||||
var file_packedinfo_proto_goTypes = []any{
|
||||
(*PackedInfo)(nil), // 0: app.protobuf.PackedInfo
|
||||
(*ImageHash)(nil), // 1: app.protobuf.ImageHash
|
||||
(*VideoHash)(nil), // 2: app.protobuf.VideoHash
|
||||
}
|
||||
var file_packedinfo_proto_depIdxs = []int32{
|
||||
1, // 0: app.protobuf.PackedInfo.image:type_name -> app.protobuf.ImageHash
|
||||
2, // 1: app.protobuf.PackedInfo.video:type_name -> app.protobuf.VideoHash
|
||||
2, // [2:2] is the sub-list for method output_type
|
||||
2, // [2:2] is the sub-list for method input_type
|
||||
2, // [2:2] is the sub-list for extension type_name
|
||||
2, // [2:2] is the sub-list for extension extendee
|
||||
0, // [0:2] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_packedinfo_proto_init() }
|
||||
func file_packedinfo_proto_init() {
|
||||
if File_packedinfo_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_packedinfo_proto_rawDesc), len(file_packedinfo_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 3,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_packedinfo_proto_goTypes,
|
||||
DependencyIndexes: file_packedinfo_proto_depIdxs,
|
||||
MessageInfos: file_packedinfo_proto_msgTypes,
|
||||
}.Build()
|
||||
File_packedinfo_proto = out.File
|
||||
file_packedinfo_proto_goTypes = nil
|
||||
file_packedinfo_proto_depIdxs = nil
|
||||
}
|
||||
19
internal/model/wxproto/packedinfo.proto
Normal file
19
internal/model/wxproto/packedinfo.proto
Normal file
@@ -0,0 +1,19 @@
|
||||
syntax = "proto3";
|
||||
package app.protobuf;
|
||||
option go_package=".;wxproto";
|
||||
|
||||
message PackedInfo {
|
||||
uint32 type = 1; // 始终为 106 (0x6a)
|
||||
uint32 version = 2; // 始终为 14 (0xe)
|
||||
ImageHash image = 3; // 图片哈希
|
||||
VideoHash video = 4; // 视频哈希
|
||||
}
|
||||
|
||||
|
||||
message ImageHash {
|
||||
string md5 = 4; // 32 字符的 MD5 哈希
|
||||
}
|
||||
|
||||
message VideoHash {
|
||||
string md5 = 8; // 32 字符的 MD5 哈希
|
||||
}
|
||||
Reference in New Issue
Block a user