support voice message
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/sjzar/chatlog/internal/errors"
|
||||
"github.com/sjzar/chatlog/pkg/util"
|
||||
"github.com/sjzar/chatlog/pkg/util/dat2img"
|
||||
"github.com/sjzar/chatlog/pkg/util/silk"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -34,6 +35,7 @@ func (s *Service) initRouter() {
|
||||
router.GET("/image/:key", s.GetImage)
|
||||
router.GET("/video/:key", s.GetVideo)
|
||||
router.GET("/file/:key", s.GetFile)
|
||||
router.GET("/voice/:key", s.GetVoice)
|
||||
router.GET("/data/*path", s.GetMediaData)
|
||||
|
||||
// MCP Server
|
||||
@@ -272,6 +274,9 @@ func (s *Service) GetVideo(c *gin.Context) {
|
||||
func (s *Service) GetFile(c *gin.Context) {
|
||||
s.GetMedia(c, "file")
|
||||
}
|
||||
func (s *Service) GetVoice(c *gin.Context) {
|
||||
s.GetMedia(c, "voice")
|
||||
}
|
||||
|
||||
func (s *Service) GetMedia(c *gin.Context, _type string) {
|
||||
key := c.Param("key")
|
||||
@@ -291,7 +296,13 @@ func (s *Service) GetMedia(c *gin.Context, _type string) {
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "/data/"+media.Path)
|
||||
switch media.Type {
|
||||
case "voice":
|
||||
s.HandleVoice(c, media.Data)
|
||||
default:
|
||||
c.Redirect(http.StatusFound, "/data/"+media.Path)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *Service) GetMediaData(c *gin.Context) {
|
||||
@@ -343,3 +354,12 @@ func (s *Service) HandleDatFile(c *gin.Context, path string) {
|
||||
c.File(path)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) HandleVoice(c *gin.Context, data []byte) {
|
||||
out, err := silk.Silk2MP3(data)
|
||||
if err != nil {
|
||||
c.Data(http.StatusOK, "audio/silk", data)
|
||||
return
|
||||
}
|
||||
c.Data(http.StatusOK, "audio/mp3", out)
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ func RootCause(err error) error {
|
||||
|
||||
func Err(c *gin.Context, err error) {
|
||||
if appErr, ok := err.(*Error); ok {
|
||||
c.JSON(appErr.Code, appErr)
|
||||
c.JSON(appErr.Code, appErr.Error())
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
@@ -47,33 +47,6 @@ type ContactV3 struct {
|
||||
Remark string `json:"Remark"`
|
||||
NickName string `json:"NickName"`
|
||||
Reserved1 int `json:"Reserved1"` // 1 自己好友或自己加入的群聊; 0 群聊成员(非好友)
|
||||
|
||||
// EncryptUserName string `json:"EncryptUserName"`
|
||||
// DelFlag int `json:"DelFlag"`
|
||||
// Type int `json:"Type"`
|
||||
// VerifyFlag int `json:"VerifyFlag"`
|
||||
// Reserved2 int `json:"Reserved2"`
|
||||
// Reserved3 string `json:"Reserved3"`
|
||||
// Reserved4 string `json:"Reserved4"`
|
||||
// LabelIDList string `json:"LabelIDList"`
|
||||
// DomainList string `json:"DomainList"`
|
||||
// ChatRoomType int `json:"ChatRoomType"`
|
||||
// PYInitial string `json:"PYInitial"`
|
||||
// QuanPin string `json:"QuanPin"`
|
||||
// RemarkPYInitial string `json:"RemarkPYInitial"`
|
||||
// RemarkQuanPin string `json:"RemarkQuanPin"`
|
||||
// BigHeadImgUrl string `json:"BigHeadImgUrl"`
|
||||
// SmallHeadImgUrl string `json:"SmallHeadImgUrl"`
|
||||
// HeadImgMd5 string `json:"HeadImgMd5"`
|
||||
// ChatRoomNotify int `json:"ChatRoomNotify"`
|
||||
// Reserved5 int `json:"Reserved5"`
|
||||
// Reserved6 string `json:"Reserved6"`
|
||||
// Reserved7 string `json:"Reserved7"`
|
||||
// ExtraBuf []byte `json:"ExtraBuf"`
|
||||
// Reserved8 int `json:"Reserved8"`
|
||||
// Reserved9 int `json:"Reserved9"`
|
||||
// Reserved10 string `json:"Reserved10"`
|
||||
// Reserved11 string `json:"Reserved11"`
|
||||
}
|
||||
|
||||
func (c *ContactV3) Wrap() *Contact {
|
||||
|
||||
@@ -40,33 +40,6 @@ type ContactDarwinV3 struct {
|
||||
M_nsRemark string `json:"m_nsRemark"`
|
||||
M_uiSex int `json:"m_uiSex"`
|
||||
M_nsAliasName string `json:"m_nsAliasName"`
|
||||
|
||||
// M_uiConType int `json:"m_uiConType"`
|
||||
// M_nsShortPY string `json:"m_nsShortPY"`
|
||||
// M_nsRemarkPYFull string `json:"m_nsRemarkPYFull"`
|
||||
// M_nsRemarkPYShort string `json:"m_nsRemarkPYShort"`
|
||||
// M_uiCertificationFlag int `json:"m_uiCertificationFlag"`
|
||||
// M_uiType int `json:"m_uiType"` // 本来想拿这个字段来区分是否是好友,但是数据比较乱,好在 darwin v3 Contact 表中没有群聊成员
|
||||
// M_nsImgStatus string `json:"m_nsImgStatus"`
|
||||
// M_uiImgKey int `json:"m_uiImgKey"`
|
||||
// M_nsHeadImgUrl string `json:"m_nsHeadImgUrl"`
|
||||
// M_nsHeadHDImgUrl string `json:"m_nsHeadHDImgUrl"`
|
||||
// M_nsHeadHDMd5 string `json:"m_nsHeadHDMd5"`
|
||||
// M_nsChatRoomMemList string `json:"m_nsChatRoomMemList"`
|
||||
// M_nsChatRoomAdminList string `json:"m_nsChatRoomAdminList"`
|
||||
// M_uiChatRoomStatus int `json:"m_uiChatRoomStatus"`
|
||||
// M_nsChatRoomDesc string `json:"m_nsChatRoomDesc"`
|
||||
// M_nsDraft string `json:"m_nsDraft"`
|
||||
// M_nsBrandIconUrl string `json:"m_nsBrandIconUrl"`
|
||||
// M_nsGoogleContactName string `json:"m_nsGoogleContactName"`
|
||||
// M_nsEncodeUserName string `json:"m_nsEncodeUserName"`
|
||||
// M_uiChatRoomVersion int `json:"m_uiChatRoomVersion"`
|
||||
// M_uiChatRoomMaxCount int `json:"m_uiChatRoomMaxCount"`
|
||||
// M_uiChatRoomType int `json:"m_uiChatRoomType"`
|
||||
// M_patSuffix string `json:"m_patSuffix"`
|
||||
// RichChatRoomDesc string `json:"richChatRoomDesc"`
|
||||
// Packed_WCContactData string `json:"_packed_WCContactData"`
|
||||
// OpenIMInfo string `json:"openIMInfo"`
|
||||
}
|
||||
|
||||
func (c *ContactDarwinV3) Wrap() *Contact {
|
||||
|
||||
@@ -30,25 +30,6 @@ type ContactV4 struct {
|
||||
Remark string `json:"remark"`
|
||||
NickName string `json:"nick_name"`
|
||||
LocalType int `json:"local_type"` // 2 群聊; 3 群聊成员(非好友); 5,6 企业微信;
|
||||
|
||||
// ID int `json:"id"`
|
||||
|
||||
// EncryptUserName string `json:"encrypt_username"`
|
||||
// Flag int `json:"flag"`
|
||||
// DeleteFlag int `json:"delete_flag"`
|
||||
// VerifyFlag int `json:"verify_flag"`
|
||||
// RemarkQuanPin string `json:"remark_quan_pin"`
|
||||
// RemarkPinYinInitial string `json:"remark_pin_yin_initial"`
|
||||
// PinYinInitial string `json:"pin_yin_initial"`
|
||||
// QuanPin string `json:"quan_pin"`
|
||||
// BigHeadUrl string `json:"big_head_url"`
|
||||
// SmallHeadUrl string `json:"small_head_url"`
|
||||
// HeadImgMd5 string `json:"head_img_md5"`
|
||||
// ChatRoomNotify int `json:"chat_room_notify"`
|
||||
// IsInChatRoom int `json:"is_in_chat_room"`
|
||||
// Description string `json:"description"`
|
||||
// ExtraBuffer []byte `json:"extra_buffer"`
|
||||
// ChatRoomType int `json:"chat_room_type"`
|
||||
}
|
||||
|
||||
func (c *ContactV4) Wrap() *Contact {
|
||||
|
||||
@@ -10,6 +10,7 @@ type Media struct {
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
Data []byte `json:"data"` // for voice
|
||||
ModifyTime int64 `json:"modifyTime"`
|
||||
}
|
||||
|
||||
|
||||
@@ -283,9 +283,34 @@ type FinderMegaVideo struct {
|
||||
}
|
||||
|
||||
type SysMsg struct {
|
||||
SysMsgTemplate SysMsgTemplate `xml:"sysmsgtemplate"`
|
||||
Type string `xml:"type,attr"`
|
||||
DelChatRoomMember *DelChatRoomMember `xml:"delchatroommember,omitempty"`
|
||||
SysMsgTemplate *SysMsgTemplate `xml:"sysmsgtemplate,omitempty"`
|
||||
}
|
||||
|
||||
// 第一种消息类型:删除群成员/二维码邀请
|
||||
type DelChatRoomMember struct {
|
||||
Plain string `xml:"plain"`
|
||||
Text string `xml:"text"`
|
||||
Link QRLink `xml:"link"`
|
||||
}
|
||||
|
||||
type QRLink struct {
|
||||
Scene string `xml:"scene"`
|
||||
Text string `xml:"text"`
|
||||
MemberList QRMemberList `xml:"memberlist"`
|
||||
QRCode string `xml:"qrcode"`
|
||||
}
|
||||
|
||||
type QRMemberList struct {
|
||||
Usernames []UsernameItem `xml:"username"`
|
||||
}
|
||||
|
||||
type UsernameItem struct {
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
// 第二种消息类型:系统消息模板
|
||||
type SysMsgTemplate struct {
|
||||
ContentTemplate ContentTemplate `xml:"content_template"`
|
||||
}
|
||||
@@ -305,7 +330,8 @@ type Link struct {
|
||||
Name string `xml:"name,attr"`
|
||||
Type string `xml:"type,attr"`
|
||||
MemberList MemberList `xml:"memberlist"`
|
||||
Separator string `xml:"separator"`
|
||||
Separator string `xml:"separator,omitempty"`
|
||||
Title string `xml:"title,omitempty"`
|
||||
}
|
||||
|
||||
type MemberList struct {
|
||||
@@ -318,6 +344,24 @@ type Member struct {
|
||||
}
|
||||
|
||||
func (s *SysMsg) String() string {
|
||||
if s.Type == "delchatroommember" {
|
||||
return s.DelChatRoomMemberString()
|
||||
}
|
||||
return s.SysMsgTemplateString()
|
||||
}
|
||||
|
||||
func (s *SysMsg) DelChatRoomMemberString() string {
|
||||
if s.DelChatRoomMember == nil {
|
||||
return ""
|
||||
}
|
||||
return s.DelChatRoomMember.Plain
|
||||
}
|
||||
|
||||
func (s *SysMsg) SysMsgTemplateString() string {
|
||||
if s.SysMsgTemplate == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
template := s.SysMsgTemplate.ContentTemplate.Template
|
||||
links := s.SysMsgTemplate.ContentTemplate.LinkList.Links
|
||||
|
||||
@@ -354,7 +398,11 @@ func (s *SysMsg) String() string {
|
||||
|
||||
// 可以根据需要添加其他链接类型的处理逻辑
|
||||
default:
|
||||
replacement = ""
|
||||
if link.Title != "" {
|
||||
replacement = link.Title
|
||||
} else {
|
||||
replacement = ""
|
||||
}
|
||||
}
|
||||
|
||||
// 将占位符名称和替换内容存入映射
|
||||
|
||||
@@ -55,6 +55,8 @@ func (m *Message) ParseMediaInfo(data string) error {
|
||||
if Debug {
|
||||
m.SysMsg = &sysMsg
|
||||
}
|
||||
m.Sender = "系统消息"
|
||||
m.SenderName = ""
|
||||
m.Content = sysMsg.String()
|
||||
return nil
|
||||
}
|
||||
@@ -188,13 +190,8 @@ func (m *Message) PlainText(showChatRoom bool, host string) string {
|
||||
buf := strings.Builder{}
|
||||
|
||||
sender := m.Sender
|
||||
switch {
|
||||
case m.Type == 10000:
|
||||
sender = "系统消息"
|
||||
case m.IsSelf:
|
||||
if m.IsSelf {
|
||||
sender = "我"
|
||||
default:
|
||||
sender = m.Sender
|
||||
}
|
||||
if m.SenderName != "" {
|
||||
buf.WriteString(m.SenderName)
|
||||
@@ -235,6 +232,9 @@ func (m *Message) PlainTextContent() string {
|
||||
case 3:
|
||||
return fmt.Sprintf("", m.Contents["host"], m.Contents["md5"])
|
||||
case 34:
|
||||
if voice, ok := m.Contents["voice"]; ok {
|
||||
return fmt.Sprintf("[语音](http://%s/voice/%s)", m.Contents["host"], voice)
|
||||
}
|
||||
return "[语音]"
|
||||
case 42:
|
||||
return "[名片]"
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -39,6 +40,7 @@ import (
|
||||
// BytesTrans BLOB
|
||||
// )
|
||||
type MessageV3 struct {
|
||||
MsgSvrID int64 `json:"MsgSvrID"` // 消息 ID
|
||||
Sequence int64 `json:"Sequence"` // 消息序号,10位时间戳 + 3位序号
|
||||
CreateTime int64 `json:"CreateTime"` // 消息创建时间,10位时间戳
|
||||
StrTalker string `json:"StrTalker"` // 聊天对象,微信 ID or 群 ID
|
||||
@@ -77,6 +79,11 @@ func (m *MessageV3) Wrap() *Message {
|
||||
|
||||
_m.ParseMediaInfo(_m.Content)
|
||||
|
||||
// 语音消息
|
||||
if _m.Type == 34 {
|
||||
_m.Contents["voice"] = fmt.Sprint(m.MsgSvrID)
|
||||
}
|
||||
|
||||
if len(m.BytesExtra) != 0 {
|
||||
if bytesExtra := ParseBytesExtra(m.BytesExtra); bytesExtra != nil {
|
||||
if _m.IsChatRoom {
|
||||
|
||||
@@ -2,6 +2,7 @@ package model
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -31,6 +32,7 @@ import (
|
||||
// )
|
||||
type MessageV4 struct {
|
||||
SortSeq int64 `json:"sort_seq"` // 消息序号,10位时间戳 + 3位序号
|
||||
ServerID int64 `json:"server_id"` // 消息 ID,用于关联 voice
|
||||
LocalType int64 `json:"local_type"` // 消息类型
|
||||
UserName string `json:"user_name"` // 发送人,通过 Join Name2Id 表获得
|
||||
CreateTime int64 `json:"create_time"` // 消息创建时间,10位时间戳
|
||||
@@ -74,6 +76,11 @@ func (m *MessageV4) Wrap(talker string) *Message {
|
||||
|
||||
_m.ParseMediaInfo(content)
|
||||
|
||||
// 语音消息
|
||||
if _m.Type == 34 {
|
||||
_m.Contents["voice"] = fmt.Sprint(m.ServerID)
|
||||
}
|
||||
|
||||
if len(m.PackedInfoData) != 0 {
|
||||
if packedInfo := ParsePackedInfo(m.PackedInfoData); packedInfo != nil {
|
||||
// FIXME 尝试解决 v4 版本 xml 数据无法匹配到 hardlink 记录的问题
|
||||
|
||||
@@ -23,6 +23,7 @@ const (
|
||||
ContactFilePattern = "^contact\\.db$"
|
||||
SessionFilePattern = "^session\\.db$"
|
||||
MediaFilePattern = "^hardlink\\.db$"
|
||||
VoiceFilePattern = "^media_([0-9]?[0-9])?\\.db$"
|
||||
)
|
||||
|
||||
// MessageDBInfo 存储消息数据库的信息
|
||||
@@ -38,6 +39,7 @@ type DataSource struct {
|
||||
contactDb *sql.DB
|
||||
sessionDb *sql.DB
|
||||
mediaDb *sql.DB
|
||||
voiceDb []*sql.DB
|
||||
|
||||
// 消息数据库信息
|
||||
messageFiles []MessageDBInfo
|
||||
@@ -47,6 +49,7 @@ func New(path string) (*DataSource, error) {
|
||||
ds := &DataSource{
|
||||
path: path,
|
||||
messageDbs: make(map[string]*sql.DB),
|
||||
voiceDb: make([]*sql.DB, 0),
|
||||
messageFiles: make([]MessageDBInfo, 0),
|
||||
}
|
||||
|
||||
@@ -62,6 +65,9 @@ func New(path string) (*DataSource, error) {
|
||||
if err := ds.initMediaDb(path); err != nil {
|
||||
return nil, errors.DBInitFailed(err)
|
||||
}
|
||||
if err := ds.initVoiceDb(path); err != nil {
|
||||
return nil, errors.DBInitFailed(err)
|
||||
}
|
||||
|
||||
return ds, nil
|
||||
}
|
||||
@@ -173,6 +179,24 @@ func (ds *DataSource) initMediaDb(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *DataSource) initVoiceDb(path string) error {
|
||||
files, err := util.FindFilesWithPatterns(path, VoiceFilePattern, true)
|
||||
if err != nil {
|
||||
return errors.DBFileNotFound(path, VoiceFilePattern, err)
|
||||
}
|
||||
if len(files) == 0 {
|
||||
return errors.DBFileNotFound(path, VoiceFilePattern, nil)
|
||||
}
|
||||
for _, file := range files {
|
||||
db, err := sql.Open("sqlite3", file)
|
||||
if err != nil {
|
||||
return errors.DBConnectFailed(files[0], err)
|
||||
}
|
||||
ds.voiceDb = append(ds.voiceDb, db)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getDBInfosForTimeRange 获取时间范围内的数据库信息
|
||||
func (ds *DataSource) getDBInfosForTimeRange(startTime, endTime time.Time) []MessageDBInfo {
|
||||
var dbs []MessageDBInfo
|
||||
@@ -188,6 +212,7 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T
|
||||
if talker == "" {
|
||||
return nil, errors.ErrTalkerEmpty
|
||||
}
|
||||
log.Debug().Msg(talker)
|
||||
|
||||
// 找到时间范围内的数据库文件
|
||||
dbInfos := ds.getDBInfosForTimeRange(startTime, endTime)
|
||||
@@ -215,7 +240,7 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T
|
||||
continue
|
||||
}
|
||||
|
||||
messages, err := ds.getMessagesFromDB(ctx, db, dbInfo, startTime, endTime, talker)
|
||||
messages, err := ds.getMessagesFromDB(ctx, db, startTime, endTime, talker)
|
||||
if err != nil {
|
||||
log.Err(err).Msgf("从数据库 %s 获取消息失败", dbInfo.FilePath)
|
||||
continue
|
||||
@@ -260,12 +285,26 @@ func (ds *DataSource) getMessagesSingleFile(ctx context.Context, dbInfo MessageD
|
||||
talkerMd5 := hex.EncodeToString(_talkerMd5Bytes[:])
|
||||
tableName := "Msg_" + talkerMd5
|
||||
|
||||
// 检查表是否存在
|
||||
var exists bool
|
||||
err := db.QueryRowContext(ctx,
|
||||
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=?",
|
||||
tableName).Scan(&exists)
|
||||
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
// 表不存在,返回空结果
|
||||
return []*model.Message{}, nil
|
||||
}
|
||||
return nil, errors.QueryFailed("", err)
|
||||
}
|
||||
|
||||
// 构建查询条件
|
||||
conditions := []string{"create_time >= ? AND create_time <= ?"}
|
||||
args := []interface{}{startTime.Unix(), endTime.Unix()}
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
SELECT m.sort_seq, m.local_type, n.user_name, m.create_time, m.message_content, m.packed_info_data, m.status
|
||||
SELECT m.sort_seq, m.server_id, 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
|
||||
@@ -293,6 +332,7 @@ func (ds *DataSource) getMessagesSingleFile(ctx context.Context, dbInfo MessageD
|
||||
var msg model.MessageV4
|
||||
err := rows.Scan(
|
||||
&msg.SortSeq,
|
||||
&msg.ServerID,
|
||||
&msg.LocalType,
|
||||
&msg.UserName,
|
||||
&msg.CreateTime,
|
||||
@@ -311,7 +351,7 @@ func (ds *DataSource) getMessagesSingleFile(ctx context.Context, dbInfo MessageD
|
||||
}
|
||||
|
||||
// getMessagesFromDB 从数据库获取消息
|
||||
func (ds *DataSource) getMessagesFromDB(ctx context.Context, db *sql.DB, dbInfo MessageDBInfo, startTime, endTime time.Time, talker string) ([]*model.Message, error) {
|
||||
func (ds *DataSource) getMessagesFromDB(ctx context.Context, db *sql.DB, startTime, endTime time.Time, talker string) ([]*model.Message, error) {
|
||||
// 构建表名
|
||||
_talkerMd5Bytes := md5.Sum([]byte(talker))
|
||||
talkerMd5 := hex.EncodeToString(_talkerMd5Bytes[:])
|
||||
@@ -336,7 +376,7 @@ func (ds *DataSource) getMessagesFromDB(ctx context.Context, db *sql.DB, dbInfo
|
||||
args := []interface{}{startTime.Unix(), endTime.Unix()}
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
SELECT m.sort_seq, m.local_type, n.user_name, m.create_time, m.message_content, m.packed_info_data, m.status
|
||||
SELECT m.sort_seq, m.server_id, 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
|
||||
@@ -361,6 +401,7 @@ func (ds *DataSource) getMessagesFromDB(ctx context.Context, db *sql.DB, dbInfo
|
||||
var msg model.MessageV4
|
||||
err := rows.Scan(
|
||||
&msg.SortSeq,
|
||||
&msg.ServerID,
|
||||
&msg.LocalType,
|
||||
&msg.UserName,
|
||||
&msg.CreateTime,
|
||||
@@ -605,10 +646,6 @@ func (ds *DataSource) GetMedia(ctx context.Context, _type string, key string) (*
|
||||
return nil, errors.ErrKeyEmpty
|
||||
}
|
||||
|
||||
if len(key) != 32 {
|
||||
return nil, errors.ErrKeyLengthMust32
|
||||
}
|
||||
|
||||
var table string
|
||||
switch _type {
|
||||
case "image":
|
||||
@@ -617,6 +654,8 @@ func (ds *DataSource) GetMedia(ctx context.Context, _type string, key string) (*
|
||||
table = "video_hardlink_info_v3"
|
||||
case "file":
|
||||
table = "file_hardlink_info_v3"
|
||||
case "voice":
|
||||
return ds.GetVoice(ctx, key)
|
||||
default:
|
||||
return nil, errors.MediaTypeUnsupported(_type)
|
||||
}
|
||||
@@ -675,6 +714,46 @@ func (ds *DataSource) GetMedia(ctx context.Context, _type string, key string) (*
|
||||
return media, nil
|
||||
}
|
||||
|
||||
func (ds *DataSource) GetVoice(ctx context.Context, key string) (*model.Media, error) {
|
||||
if key == "" {
|
||||
return nil, errors.ErrKeyEmpty
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT voice_data
|
||||
FROM VoiceInfo
|
||||
WHERE svr_id = ?
|
||||
`
|
||||
args := []interface{}{key}
|
||||
|
||||
for _, db := range ds.voiceDb {
|
||||
rows, err := db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, errors.QueryFailed(query, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var voiceData []byte
|
||||
err := rows.Scan(
|
||||
&voiceData,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.ScanRowFailed(err)
|
||||
}
|
||||
if len(voiceData) > 0 {
|
||||
return &model.Media{
|
||||
Type: "voice",
|
||||
Key: key,
|
||||
Data: voiceData,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.ErrMediaNotFound
|
||||
}
|
||||
|
||||
func (ds *DataSource) Close() error {
|
||||
var errs []error
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ const (
|
||||
ImageFilePattern = "^HardLinkImage\\.db$"
|
||||
VideoFilePattern = "^HardLinkVideo\\.db$"
|
||||
FileFilePattern = "^HardLinkFile\\.db$"
|
||||
VoiceFilePattern = "^MediaMSG([0-9])?\\.db$"
|
||||
)
|
||||
|
||||
// MessageDBInfo 保存消息数据库的信息
|
||||
@@ -46,6 +47,7 @@ type DataSource struct {
|
||||
imageDb *sql.DB
|
||||
videoDb *sql.DB
|
||||
fileDb *sql.DB
|
||||
voiceDb []*sql.DB
|
||||
}
|
||||
|
||||
// New 创建一个新的 WindowsV3DataSource
|
||||
@@ -53,6 +55,7 @@ func New(path string) (*DataSource, error) {
|
||||
ds := &DataSource{
|
||||
messageFiles: make([]MessageDBInfo, 0),
|
||||
messageDbs: make(map[string]*sql.DB),
|
||||
voiceDb: make([]*sql.DB, 0),
|
||||
}
|
||||
|
||||
// 初始化消息数据库
|
||||
@@ -69,6 +72,10 @@ func New(path string) (*DataSource, error) {
|
||||
return nil, errors.DBInitFailed(err)
|
||||
}
|
||||
|
||||
if err := ds.initVoiceDb(path); err != nil {
|
||||
return nil, errors.DBInitFailed(err)
|
||||
}
|
||||
|
||||
return ds, nil
|
||||
}
|
||||
|
||||
@@ -238,6 +245,24 @@ func (ds *DataSource) initMediaDb(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *DataSource) initVoiceDb(path string) error {
|
||||
files, err := util.FindFilesWithPatterns(path, VoiceFilePattern, true)
|
||||
if err != nil {
|
||||
return errors.DBFileNotFound(path, VoiceFilePattern, err)
|
||||
}
|
||||
if len(files) == 0 {
|
||||
return errors.DBFileNotFound(path, VoiceFilePattern, nil)
|
||||
}
|
||||
for _, file := range files {
|
||||
db, err := sql.Open("sqlite3", file)
|
||||
if err != nil {
|
||||
return errors.DBConnectFailed(files[0], err)
|
||||
}
|
||||
ds.voiceDb = append(ds.voiceDb, db)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// getDBInfosForTimeRange 获取时间范围内的数据库信息
|
||||
func (ds *DataSource) getDBInfosForTimeRange(startTime, endTime time.Time) []MessageDBInfo {
|
||||
var dbs []MessageDBInfo
|
||||
@@ -293,7 +318,7 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
SELECT Sequence, CreateTime, StrTalker, IsSender,
|
||||
SELECT MsgSvrID, Sequence, CreateTime, StrTalker, IsSender,
|
||||
Type, SubType, StrContent, CompressContent, BytesExtra
|
||||
FROM MSG
|
||||
WHERE %s
|
||||
@@ -314,6 +339,7 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T
|
||||
var bytesExtra []byte
|
||||
|
||||
err := rows.Scan(
|
||||
&msg.MsgSvrID,
|
||||
&msg.Sequence,
|
||||
&msg.CreateTime,
|
||||
&msg.StrTalker,
|
||||
@@ -377,7 +403,7 @@ func (ds *DataSource) getMessagesSingleFile(ctx context.Context, dbInfo MessageD
|
||||
}
|
||||
}
|
||||
query := fmt.Sprintf(`
|
||||
SELECT Sequence, CreateTime, StrTalker, IsSender,
|
||||
SELECT MsgSvrID, Sequence, CreateTime, StrTalker, IsSender,
|
||||
Type, SubType, StrContent, CompressContent, BytesExtra
|
||||
FROM MSG
|
||||
WHERE %s
|
||||
@@ -406,6 +432,7 @@ func (ds *DataSource) getMessagesSingleFile(ctx context.Context, dbInfo MessageD
|
||||
var compressContent []byte
|
||||
var bytesExtra []byte
|
||||
err := rows.Scan(
|
||||
&msg.MsgSvrID,
|
||||
&msg.Sequence,
|
||||
&msg.CreateTime,
|
||||
&msg.StrTalker,
|
||||
@@ -652,6 +679,10 @@ func (ds *DataSource) GetMedia(ctx context.Context, _type string, key string) (*
|
||||
return nil, errors.ErrKeyEmpty
|
||||
}
|
||||
|
||||
if _type == "voice" {
|
||||
return ds.GetVoice(ctx, key)
|
||||
}
|
||||
|
||||
md5key, err := hex.DecodeString(key)
|
||||
if err != nil {
|
||||
return nil, errors.DecodeKeyFailed(err)
|
||||
@@ -725,6 +756,46 @@ func (ds *DataSource) GetMedia(ctx context.Context, _type string, key string) (*
|
||||
return media, nil
|
||||
}
|
||||
|
||||
func (ds *DataSource) GetVoice(ctx context.Context, key string) (*model.Media, error) {
|
||||
if key == "" {
|
||||
return nil, errors.ErrKeyEmpty
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT Buf
|
||||
FROM Media
|
||||
WHERE Reserved0 = ?
|
||||
`
|
||||
args := []interface{}{key}
|
||||
|
||||
for _, db := range ds.voiceDb {
|
||||
rows, err := db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, errors.QueryFailed(query, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
for rows.Next() {
|
||||
var voiceData []byte
|
||||
err := rows.Scan(
|
||||
&voiceData,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.ScanRowFailed(err)
|
||||
}
|
||||
if len(voiceData) > 0 {
|
||||
return &model.Media{
|
||||
Type: "voice",
|
||||
Key: key,
|
||||
Data: voiceData,
|
||||
}, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil, errors.ErrMediaNotFound
|
||||
}
|
||||
|
||||
// Close 实现 DataSource 接口的 Close 方法
|
||||
func (ds *DataSource) Close() error {
|
||||
var errs []error
|
||||
|
||||
Reference in New Issue
Block a user