This commit is contained in:
Shen Junzheng
2025-03-21 21:45:08 +08:00
parent 78cce92ce3
commit 80c7e67106
86 changed files with 7061 additions and 2316 deletions

View File

@@ -1,269 +0,0 @@
package wechatdb
import (
"database/sql"
"fmt"
"github.com/sjzar/chatlog/pkg/model"
"github.com/sjzar/chatlog/pkg/util"
log "github.com/sirupsen/logrus"
)
var (
ContactFileV3 = "^MicroMsg.db$"
ContactFileV4 = "contact.db$"
)
type Contact struct {
version int
dbFile string
db *sql.DB
Contact map[string]*model.Contact // 好友和群聊信息Key UserName
ChatRoom map[string]*model.ChatRoom // 群聊信息Key UserName
Sessions []*model.Session // 历史会话,按时间倒序
// Quick Search
ChatRoomUsers map[string]*model.Contact // 群聊成员信息Key UserName
Alias2Contack map[string]*model.Contact // 别名到联系人的映射
Remark2Contack map[string]*model.Contact // 备注名到联系人的映射
NickName2Contack map[string]*model.Contact // 昵称到联系人的映射
}
func NewContact(path string, version int) (*Contact, error) {
c := &Contact{
version: version,
}
files, err := util.FindFilesWithPatterns(path, ContactFileV3, true)
if err != nil {
return nil, fmt.Errorf("查找数据库文件失败: %v", err)
}
if len(files) == 0 {
return nil, fmt.Errorf("未找到任何数据库文件: %s", path)
}
c.dbFile = files[0]
c.db, err = sql.Open("sqlite3", c.dbFile)
if err != nil {
log.Printf("警告: 连接数据库 %s 失败: %v", c.dbFile, err)
return nil, fmt.Errorf("连接数据库失败: %v", err)
}
c.loadContact()
c.loadChatRoom()
c.loadSession()
c.fillChatRoomInfo()
return c, nil
}
func (c *Contact) loadContact() {
contactMap := make(map[string]*model.Contact)
chatRoomUserMap := make(map[string]*model.Contact)
aliasMap := make(map[string]*model.Contact)
remarkMap := make(map[string]*model.Contact)
nickNameMap := make(map[string]*model.Contact)
rows, err := c.db.Query("SELECT UserName, Alias, Remark, NickName, Reserved1 FROM Contact")
if err != nil {
log.Errorf("查询联系人失败: %v", err)
return
}
for rows.Next() {
var contactv3 model.ContactV3
if err := rows.Scan(
&contactv3.UserName,
&contactv3.Alias,
&contactv3.Remark,
&contactv3.NickName,
&contactv3.Reserved1,
); err != nil {
log.Printf("警告: 扫描联系人行失败: %v", err)
continue
}
contact := contactv3.Wrap()
if contact.IsFriend {
contactMap[contact.UserName] = contact
if contact.Alias != "" {
aliasMap[contact.Alias] = contact
}
if contact.Remark != "" {
remarkMap[contact.Remark] = contact
}
if contact.NickName != "" {
nickNameMap[contact.NickName] = contact
}
} else {
chatRoomUserMap[contact.UserName] = contact
}
}
rows.Close()
c.Contact = contactMap
c.ChatRoomUsers = chatRoomUserMap
c.Alias2Contack = aliasMap
c.Remark2Contack = remarkMap
c.NickName2Contack = nickNameMap
}
func (c *Contact) loadChatRoom() {
chatRoomMap := make(map[string]*model.ChatRoom)
rows, err := c.db.Query("SELECT ChatRoomName, Reserved2, RoomData FROM ChatRoom")
if err != nil {
log.Errorf("查询群聊失败: %v", err)
return
}
for rows.Next() {
var chatRoom model.ChatRoomV3
if err := rows.Scan(
&chatRoom.ChatRoomName,
&chatRoom.Reserved2,
&chatRoom.RoomData,
); err != nil {
log.Printf("警告: 扫描群聊行失败: %v", err)
continue
}
chatRoomMap[chatRoom.ChatRoomName] = chatRoom.Wrap()
}
rows.Close()
c.ChatRoom = chatRoomMap
}
func (c *Contact) loadSession() {
sessions := make([]*model.Session, 0)
rows, err := c.db.Query("SELECT strUsrName, nOrder, strNickName, strContent, nTime FROM Session ORDER BY nOrder DESC")
if err != nil {
log.Errorf("查询群聊失败: %v", err)
return
}
for rows.Next() {
var sessionV3 model.SessionV3
if err := rows.Scan(
&sessionV3.StrUsrName,
&sessionV3.NOrder,
&sessionV3.StrNickName,
&sessionV3.StrContent,
&sessionV3.NTime,
); err != nil {
log.Printf("警告: 扫描历史会话失败: %v", err)
continue
}
session := sessionV3.Wrap()
sessions = append(sessions, session)
}
rows.Close()
c.Sessions = sessions
}
func (c *Contact) ListContact() ([]*model.Contact, error) {
contacts := make([]*model.Contact, 0, len(c.Contact))
for _, contact := range c.Contact {
contacts = append(contacts, contact)
}
return contacts, nil
}
func (c *Contact) ListChatRoom() ([]*model.ChatRoom, error) {
chatRooms := make([]*model.ChatRoom, 0, len(c.ChatRoom))
for _, chatRoom := range c.ChatRoom {
chatRooms = append(chatRooms, chatRoom)
}
return chatRooms, nil
}
func (c *Contact) GetContact(key string) *model.Contact {
if contact, ok := c.Contact[key]; ok {
return contact
}
if contact, ok := c.Alias2Contack[key]; ok {
return contact
}
if contact, ok := c.Remark2Contack[key]; ok {
return contact
}
if contact, ok := c.NickName2Contack[key]; ok {
return contact
}
return nil
}
func (c *Contact) GetChatRoom(name string) *model.ChatRoom {
if chatRoom, ok := c.ChatRoom[name]; ok {
return chatRoom
}
if contact := c.GetContact(name); contact != nil {
if chatRoom, ok := c.ChatRoom[contact.UserName]; ok {
return chatRoom
} else {
// 被删除的群聊,在 ChatRoom 记录中没有了,但是能找到 Contact做下 Mock
return &model.ChatRoom{
Name: contact.UserName,
Remark: contact.Remark,
NickName: contact.NickName,
Users: make([]model.ChatRoomUser, 0),
User2DisplayName: make(map[string]string),
}
}
}
return nil
}
func (c *Contact) GetSession(limit int) []*model.Session {
if limit <= 0 {
limit = len(c.Sessions)
}
if len(c.Sessions) < limit {
limit = len(c.Sessions)
}
return c.Sessions[:limit]
}
func (c *Contact) getFullContact(userName string) *model.Contact {
if contact := c.GetContact(userName); contact != nil {
return contact
}
if contact, ok := c.ChatRoomUsers[userName]; ok {
return contact
}
return nil
}
func (c *Contact) fillChatRoomInfo() {
for i := range c.ChatRoom {
if contact := c.GetContact(c.ChatRoom[i].Name); contact != nil {
c.ChatRoom[i].Remark = contact.Remark
c.ChatRoom[i].NickName = contact.NickName
}
}
}
func (c *Contact) MessageFillInfo(msg *model.Message) {
talker := msg.Talker
if msg.IsChatRoom {
talker = msg.ChatRoomSender
if chatRoom := c.GetChatRoom(msg.Talker); chatRoom != nil {
msg.CharRoomName = chatRoom.DisplayName()
if displayName, ok := chatRoom.User2DisplayName[talker]; ok {
msg.DisplayName = displayName
}
}
}
if msg.DisplayName == "" && msg.IsSender != 1 {
if contact := c.getFullContact(talker); contact != nil {
msg.DisplayName = contact.DisplayName()
}
}
}

View File

@@ -0,0 +1,510 @@
package darwinv3
import (
"context"
"crypto/md5"
"database/sql"
"encoding/hex"
"fmt"
"log"
"strings"
"time"
"github.com/sjzar/chatlog/internal/model"
"github.com/sjzar/chatlog/pkg/util"
_ "github.com/mattn/go-sqlite3"
)
const (
MessageFilePattern = "^msg_([0-9]?[0-9])?\\.db$"
ContactFilePattern = "^wccontact_new2\\.db$"
ChatRoomFilePattern = "^group_new\\.db$"
SessionFilePattern = "^session_new\\.db$"
)
type DataSource struct {
path string
messageDbs []*sql.DB
contactDb *sql.DB
chatRoomDb *sql.DB
sessionDb *sql.DB
talkerDBMap map[string]*sql.DB
user2DisplayName map[string]string
}
func New(path string) (*DataSource, error) {
ds := &DataSource{
path: path,
messageDbs: make([]*sql.DB, 0),
talkerDBMap: make(map[string]*sql.DB),
user2DisplayName: make(map[string]string),
}
if err := ds.initMessageDbs(path); err != nil {
return nil, fmt.Errorf("初始化消息数据库失败: %w", err)
}
if err := ds.initContactDb(path); err != nil {
return nil, fmt.Errorf("初始化联系人数据库失败: %w", err)
}
if err := ds.initChatRoomDb(path); err != nil {
return nil, fmt.Errorf("初始化群聊数据库失败: %w", err)
}
if err := ds.initSessionDb(path); err != nil {
return nil, fmt.Errorf("初始化会话数据库失败: %w", err)
}
return ds, nil
}
func (ds *DataSource) initMessageDbs(path string) error {
files, err := util.FindFilesWithPatterns(path, MessageFilePattern, true)
if err != nil {
return fmt.Errorf("查找消息数据库文件失败: %w", err)
}
if len(files) == 0 {
return fmt.Errorf("未找到任何消息数据库文件: %s", path)
}
// 处理每个数据库文件
for _, filePath := range files {
// 连接数据库
db, err := sql.Open("sqlite3", filePath)
if err != nil {
log.Printf("警告: 连接数据库 %s 失败: %v", filePath, err)
continue
}
ds.messageDbs = append(ds.messageDbs, db)
// 获取所有表名
rows, err := db.Query("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Chat_%'")
if err != nil {
log.Printf("警告: 获取表名失败: %v", err)
continue
}
for rows.Next() {
var tableName string
if err := rows.Scan(&tableName); err != nil {
log.Printf("警告: 扫描表名失败: %v", err)
continue
}
// 从表名中提取可能的talker信息
talkerMd5 := extractTalkerFromTableName(tableName)
if talkerMd5 == "" {
continue
}
ds.talkerDBMap[talkerMd5] = db
}
rows.Close()
}
return nil
}
func (ds *DataSource) initContactDb(path string) error {
files, err := util.FindFilesWithPatterns(path, ContactFilePattern, true)
if err != nil {
return fmt.Errorf("查找联系人数据库文件失败: %w", err)
}
if len(files) == 0 {
return fmt.Errorf("未找到联系人数据库文件: %s", path)
}
ds.contactDb, err = sql.Open("sqlite3", files[0])
if err != nil {
return fmt.Errorf("连接联系人数据库失败: %w", err)
}
return nil
}
func (ds *DataSource) initChatRoomDb(path string) error {
files, err := util.FindFilesWithPatterns(path, ChatRoomFilePattern, true)
if err != nil {
return fmt.Errorf("查找群聊数据库文件失败: %w", err)
}
if len(files) == 0 {
return fmt.Errorf("未找到群聊数据库文件: %s", path)
}
ds.chatRoomDb, err = sql.Open("sqlite3", files[0])
if err != nil {
return fmt.Errorf("连接群聊数据库失败: %w", err)
}
rows, err := ds.chatRoomDb.Query("SELECT m_nsUsrName, nickname FROM GroupMember")
if err != nil {
log.Printf("警告: 获取群聊成员失败: %v", err)
return nil
}
for rows.Next() {
var user string
var nickName string
if err := rows.Scan(&user, &nickName); err != nil {
log.Printf("警告: 扫描表名失败: %v", err)
continue
}
ds.user2DisplayName[user] = nickName
}
rows.Close()
return nil
}
func (ds *DataSource) initSessionDb(path string) error {
files, err := util.FindFilesWithPatterns(path, SessionFilePattern, true)
if err != nil {
return fmt.Errorf("查找最近会话数据库文件失败: %w", err)
}
if len(files) == 0 {
return fmt.Errorf("未找到最近会话数据库文件: %s", path)
}
ds.sessionDb, err = sql.Open("sqlite3", files[0])
if err != nil {
return fmt.Errorf("连接最近会话数据库失败: %w", err)
}
return nil
}
// GetMessages 实现获取消息的方法
func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.Time, talker string, limit, offset int) ([]*model.Message, error) {
// 在 darwinv3 中,每个联系人/群聊的消息存储在单独的表中,表名为 Chat_md5(talker)
// 首先需要找到对应的表名
if talker == "" {
return nil, fmt.Errorf("talker 不能为空")
}
_talkerMd5Bytes := md5.Sum([]byte(talker))
talkerMd5 := hex.EncodeToString(_talkerMd5Bytes[:])
db, ok := ds.talkerDBMap[talkerMd5]
if !ok {
return nil, fmt.Errorf("未找到 talker %s 的消息数据库", talker)
}
tableName := fmt.Sprintf("Chat_%s", talkerMd5)
// 构建查询条件
query := fmt.Sprintf(`
SELECT msgCreateTime, msgContent, messageType, mesDes, msgSource, CompressContent, ConBlob
FROM %s
WHERE msgCreateTime >= ? AND msgCreateTime <= ?
ORDER BY msgCreateTime ASC
`, tableName)
if limit > 0 {
query += fmt.Sprintf(" LIMIT %d", limit)
if offset > 0 {
query += fmt.Sprintf(" OFFSET %d", offset)
}
}
// 执行查询
rows, err := db.QueryContext(ctx, query, startTime.Unix(), endTime.Unix())
if err != nil {
return nil, fmt.Errorf("查询表 %s 失败: %w", tableName, err)
}
defer rows.Close()
// 处理查询结果
messages := []*model.Message{}
for rows.Next() {
var msg model.MessageDarwinV3
var compressContent, conBlob []byte
err := rows.Scan(
&msg.MesCreateTime,
&msg.MesContent,
&msg.MesType,
&msg.MesDes,
&msg.MesSource,
&compressContent,
&conBlob,
)
if err != nil {
log.Printf("警告: 扫描消息行失败: %v", err)
continue
}
// 将消息包装为通用模型
message := msg.Wrap(talker)
messages = append(messages, message)
}
return messages, nil
}
// 从表名中提取 talker
func extractTalkerFromTableName(tableName string) string {
if !strings.HasPrefix(tableName, "Chat_") {
return ""
}
if strings.HasSuffix(tableName, "_dels") {
return ""
}
return strings.TrimPrefix(tableName, "Chat_")
}
// GetContacts 实现获取联系人信息的方法
func (ds *DataSource) GetContacts(ctx context.Context, key string, limit, offset int) ([]*model.Contact, error) {
var query string
var args []interface{}
if key != "" {
// 按照关键字查询
query = `SELECT m_nsUsrName, nickname, IFNULL(m_nsRemark,""), m_uiSex, IFNULL(m_nsAliasName,"")
FROM WCContact
WHERE m_nsUsrName = ? OR nickname = ? OR m_nsRemark = ? OR m_nsAliasName = ?`
args = []interface{}{key, key, key, key}
} else {
// 查询所有联系人
query = `SELECT m_nsUsrName, nickname, IFNULL(m_nsRemark,""), m_uiSex, IFNULL(m_nsAliasName,"")
FROM WCContact`
}
// 添加排序、分页
query += ` ORDER BY m_nsUsrName`
if limit > 0 {
query += fmt.Sprintf(" LIMIT %d", limit)
if offset > 0 {
query += fmt.Sprintf(" OFFSET %d", offset)
}
}
// 执行查询
rows, err := ds.contactDb.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("查询联系人失败: %w", err)
}
defer rows.Close()
contacts := []*model.Contact{}
for rows.Next() {
var contactDarwinV3 model.ContactDarwinV3
err := rows.Scan(
&contactDarwinV3.M_nsUsrName,
&contactDarwinV3.Nickname,
&contactDarwinV3.M_nsRemark,
&contactDarwinV3.M_uiSex,
&contactDarwinV3.M_nsAliasName,
)
if err != nil {
return nil, fmt.Errorf("扫描联系人行失败: %w", err)
}
contacts = append(contacts, contactDarwinV3.Wrap())
}
return contacts, nil
}
// GetChatRooms 实现获取群聊信息的方法
func (ds *DataSource) GetChatRooms(ctx context.Context, key string, limit, offset int) ([]*model.ChatRoom, error) {
var query string
var args []interface{}
if key != "" {
// 按照关键字查询
query = `SELECT m_nsUsrName, nickname, IFNULL(m_nsRemark,""), IFNULL(m_nsChatRoomMemList,""), IFNULL(m_nsChatRoomAdminList,"")
FROM GroupContact
WHERE m_nsUsrName = ? OR nickname = ? OR m_nsRemark = ?`
args = []interface{}{key, key, key}
} else {
// 查询所有群聊
query = `SELECT m_nsUsrName, nickname, IFNULL(m_nsRemark,""), IFNULL(m_nsChatRoomMemList,""), IFNULL(m_nsChatRoomAdminList,"")
FROM GroupContact`
}
// 添加排序、分页
query += ` ORDER BY m_nsUsrName`
if limit > 0 {
query += fmt.Sprintf(" LIMIT %d", limit)
if offset > 0 {
query += fmt.Sprintf(" OFFSET %d", offset)
}
}
// 执行查询
rows, err := ds.chatRoomDb.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("查询群聊失败: %w", err)
}
defer rows.Close()
chatRooms := []*model.ChatRoom{}
for rows.Next() {
var chatRoomDarwinV3 model.ChatRoomDarwinV3
err := rows.Scan(
&chatRoomDarwinV3.M_nsUsrName,
&chatRoomDarwinV3.Nickname,
&chatRoomDarwinV3.M_nsRemark,
&chatRoomDarwinV3.M_nsChatRoomMemList,
&chatRoomDarwinV3.M_nsChatRoomAdminList,
)
if err != nil {
return nil, fmt.Errorf("扫描群聊行失败: %w", err)
}
chatRooms = append(chatRooms, chatRoomDarwinV3.Wrap(ds.user2DisplayName))
}
// 如果没有找到群聊,尝试通过联系人查找
if len(chatRooms) == 0 && key != "" {
contacts, err := ds.GetContacts(ctx, key, 1, 0)
if err == nil && len(contacts) > 0 && strings.HasSuffix(contacts[0].UserName, "@chatroom") {
// 再次尝试通过用户名查找群聊
rows, err := ds.chatRoomDb.QueryContext(ctx,
`SELECT m_nsUsrName, nickname, m_nsRemark, m_nsChatRoomMemList, m_nsChatRoomAdminList
FROM GroupContact
WHERE m_nsUsrName = ?`,
contacts[0].UserName)
if err != nil {
return nil, fmt.Errorf("查询群聊失败: %w", err)
}
defer rows.Close()
for rows.Next() {
var chatRoomDarwinV3 model.ChatRoomDarwinV3
err := rows.Scan(
&chatRoomDarwinV3.M_nsUsrName,
&chatRoomDarwinV3.Nickname,
&chatRoomDarwinV3.M_nsRemark,
&chatRoomDarwinV3.M_nsChatRoomMemList,
&chatRoomDarwinV3.M_nsChatRoomAdminList,
)
if err != nil {
return nil, fmt.Errorf("扫描群聊行失败: %w", err)
}
chatRooms = append(chatRooms, chatRoomDarwinV3.Wrap(ds.user2DisplayName))
}
// 如果群聊记录不存在,但联系人记录存在,创建一个模拟的群聊对象
if len(chatRooms) == 0 {
chatRooms = append(chatRooms, &model.ChatRoom{
Name: contacts[0].UserName,
Users: make([]model.ChatRoomUser, 0),
})
}
}
}
return chatRooms, nil
}
// GetSessions 实现获取会话信息的方法
func (ds *DataSource) GetSessions(ctx context.Context, key string, limit, offset int) ([]*model.Session, error) {
var query string
var args []interface{}
if key != "" {
// 按照关键字查询
query = `SELECT m_nsUserName, m_uLastTime
FROM SessionAbstract
WHERE m_nsUserName = ?`
args = []interface{}{key}
} else {
// 查询所有会话
query = `SELECT m_nsUserName, m_uLastTime
FROM SessionAbstract`
}
// 添加排序、分页
query += ` ORDER BY m_uLastTime DESC`
if limit > 0 {
query += fmt.Sprintf(" LIMIT %d", limit)
if offset > 0 {
query += fmt.Sprintf(" OFFSET %d", offset)
}
}
// 执行查询
rows, err := ds.sessionDb.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("查询会话失败: %w", err)
}
defer rows.Close()
sessions := []*model.Session{}
for rows.Next() {
var sessionDarwinV3 model.SessionDarwinV3
err := rows.Scan(
&sessionDarwinV3.M_nsUserName,
&sessionDarwinV3.M_uLastTime,
)
if err != nil {
return nil, fmt.Errorf("扫描会话行失败: %w", err)
}
// 包装成通用模型
session := sessionDarwinV3.Wrap()
// 尝试获取联系人信息以补充会话信息
contacts, err := ds.GetContacts(ctx, session.UserName, 1, 0)
if err == nil && len(contacts) > 0 {
session.NickName = contacts[0].DisplayName()
} else {
// 尝试获取群聊信息
chatRooms, err := ds.GetChatRooms(ctx, session.UserName, 1, 0)
if err == nil && len(chatRooms) > 0 {
session.NickName = chatRooms[0].DisplayName()
}
}
sessions = append(sessions, session)
}
return sessions, nil
}
// Close 实现关闭数据库连接的方法
func (ds *DataSource) Close() error {
var errs []error
// 关闭消息数据库连接
for i, db := range ds.messageDbs {
if err := db.Close(); err != nil {
errs = append(errs, fmt.Errorf("关闭消息数据库 %d 失败: %w", i, err))
}
}
// 关闭联系人数据库连接
if ds.contactDb != nil {
if err := ds.contactDb.Close(); err != nil {
errs = append(errs, fmt.Errorf("关闭联系人数据库失败: %w", err))
}
}
// 关闭群聊数据库连接
if ds.chatRoomDb != nil {
if err := ds.chatRoomDb.Close(); err != nil {
errs = append(errs, fmt.Errorf("关闭群聊数据库失败: %w", err))
}
}
// 关闭会话数据库连接
if ds.sessionDb != nil {
if err := ds.sessionDb.Close(); err != nil {
errs = append(errs, fmt.Errorf("关闭会话数据库失败: %w", err))
}
}
if len(errs) > 0 {
return fmt.Errorf("关闭数据库连接时发生错误: %v", errs)
}
return nil
}

View File

@@ -0,0 +1,49 @@
package datasource
import (
"context"
"fmt"
"time"
"github.com/sjzar/chatlog/internal/model"
"github.com/sjzar/chatlog/internal/wechatdb/datasource/darwinv3"
v4 "github.com/sjzar/chatlog/internal/wechatdb/datasource/v4"
"github.com/sjzar/chatlog/internal/wechatdb/datasource/windowsv3"
)
// 错误定义
var (
ErrUnsupportedPlatform = fmt.Errorf("unsupported platform")
)
type DataSource interface {
// 消息
GetMessages(ctx context.Context, startTime, endTime time.Time, talker string, limit, offset int) ([]*model.Message, error)
// 联系人
GetContacts(ctx context.Context, key string, limit, offset int) ([]*model.Contact, error)
// 群聊
GetChatRooms(ctx context.Context, key string, limit, offset int) ([]*model.ChatRoom, error)
// 最近会话
GetSessions(ctx context.Context, key string, limit, offset int) ([]*model.Session, error)
Close() error
}
func NewDataSource(path string, platform string, version int) (DataSource, error) {
switch {
case platform == "windows" && version == 3:
return windowsv3.New(path)
case platform == "windows" && version == 4:
return v4.New(path)
case platform == "darwin" && version == 3:
return darwinv3.New(path)
case platform == "darwin" && version == 4:
return v4.New(path)
default:
return nil, fmt.Errorf("%w: %s v%d", ErrUnsupportedPlatform, platform, version)
}
}

View File

@@ -0,0 +1,634 @@
package v4
import (
"context"
"crypto/md5"
"database/sql"
"encoding/hex"
"fmt"
"log"
"sort"
"strings"
"time"
"github.com/sjzar/chatlog/internal/model"
"github.com/sjzar/chatlog/pkg/util"
_ "github.com/mattn/go-sqlite3"
)
const (
MessageFilePattern = "^message_([0-9]?[0-9])?\\.db$"
ContactFilePattern = "^contact\\.db$"
SessionFilePattern = "^session\\.db$"
)
// MessageDBInfo 存储消息数据库的信息
type MessageDBInfo struct {
FilePath string
StartTime time.Time
EndTime time.Time
ID2Name map[int]string
}
type DataSource struct {
path string
messageDbs map[string]*sql.DB
contactDb *sql.DB
sessionDb *sql.DB
// 消息数据库信息
messageFiles []MessageDBInfo
}
func New(path string) (*DataSource, error) {
ds := &DataSource{
path: path,
messageDbs: make(map[string]*sql.DB),
messageFiles: make([]MessageDBInfo, 0),
}
if err := ds.initMessageDbs(path); err != nil {
return nil, fmt.Errorf("初始化消息数据库失败: %w", err)
}
if err := ds.initContactDb(path); err != nil {
return nil, fmt.Errorf("初始化联系人数据库失败: %w", err)
}
if err := ds.initSessionDb(path); err != nil {
return nil, fmt.Errorf("初始化会话数据库失败: %w", err)
}
return ds, nil
}
func (ds *DataSource) initMessageDbs(path string) error {
// 查找所有消息数据库文件
files, err := util.FindFilesWithPatterns(path, MessageFilePattern, true)
if err != nil {
return fmt.Errorf("查找消息数据库文件失败: %w", err)
}
if len(files) == 0 {
return fmt.Errorf("未找到任何消息数据库文件: %s", path)
}
// 处理每个数据库文件
for _, filePath := range files {
// 连接数据库
db, err := sql.Open("sqlite3", filePath)
if err != nil {
log.Printf("警告: 连接数据库 %s 失败: %v", filePath, err)
continue
}
// 获取 Timestamp 表中的开始时间
var startTime time.Time
var timestamp int64
row := db.QueryRow("SELECT timestamp FROM Timestamp LIMIT 1")
if err := row.Scan(&timestamp); err != nil {
log.Printf("警告: 获取数据库 %s 的时间戳失败: %v", filePath, err)
db.Close()
continue
}
startTime = time.Unix(timestamp, 0)
// 获取 ID2Name 映射
id2Name := make(map[int]string)
rows, err := db.Query("SELECT user_name FROM Name2Id")
if err != nil {
log.Printf("警告: 获取数据库 %s 的 Name2Id 表失败: %v", filePath, err)
db.Close()
continue
}
i := 1
for rows.Next() {
var name string
if err := rows.Scan(&name); err != nil {
log.Printf("警告: 扫描 Name2Id 行失败: %v", err)
continue
}
id2Name[i] = name
i++
}
rows.Close()
// 保存数据库信息
ds.messageFiles = append(ds.messageFiles, MessageDBInfo{
FilePath: filePath,
StartTime: startTime,
ID2Name: id2Name,
})
// 保存数据库连接
ds.messageDbs[filePath] = db
}
// 按照 StartTime 排序数据库文件
sort.Slice(ds.messageFiles, func(i, j int) bool {
return ds.messageFiles[i].StartTime.Before(ds.messageFiles[j].StartTime)
})
// 设置结束时间
for i := range ds.messageFiles {
if i == len(ds.messageFiles)-1 {
ds.messageFiles[i].EndTime = time.Now()
} else {
ds.messageFiles[i].EndTime = ds.messageFiles[i+1].StartTime
}
}
return nil
}
func (ds *DataSource) initContactDb(path string) error {
files, err := util.FindFilesWithPatterns(path, ContactFilePattern, true)
if err != nil {
return fmt.Errorf("查找联系人数据库文件失败: %w", err)
}
if len(files) == 0 {
return fmt.Errorf("未找到联系人数据库文件: %s", path)
}
ds.contactDb, err = sql.Open("sqlite3", files[0])
if err != nil {
return fmt.Errorf("连接联系人数据库失败: %w", err)
}
return nil
}
func (ds *DataSource) initSessionDb(path string) error {
files, err := util.FindFilesWithPatterns(path, SessionFilePattern, true)
if err != nil {
return fmt.Errorf("查找最近会话数据库文件失败: %w", err)
}
if len(files) == 0 {
return fmt.Errorf("未找到最近会话数据库文件: %s", path)
}
ds.sessionDb, err = sql.Open("sqlite3", files[0])
if err != nil {
return fmt.Errorf("连接最近会话数据库失败: %w", err)
}
return nil
}
// getDBInfosForTimeRange 获取时间范围内的数据库信息
func (ds *DataSource) getDBInfosForTimeRange(startTime, endTime time.Time) []MessageDBInfo {
var dbs []MessageDBInfo
for _, info := range ds.messageFiles {
if info.StartTime.Before(endTime) && info.EndTime.After(startTime) {
dbs = append(dbs, info)
}
}
return dbs
}
func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.Time, talker string, limit, offset int) ([]*model.Message, error) {
if talker == "" {
return nil, fmt.Errorf("必须指定 talker 参数")
}
// 找到时间范围内的数据库文件
dbInfos := ds.getDBInfosForTimeRange(startTime, endTime)
if len(dbInfos) == 0 {
return nil, fmt.Errorf("未找到时间范围 %v 到 %v 内的数据库文件", startTime, endTime)
}
if len(dbInfos) == 1 {
// LIMIT 和 OFFSET 逻辑在单文件情况下可以直接在 SQL 里处理
return ds.getMessagesSingleFile(ctx, dbInfos[0], startTime, endTime, talker, limit, offset)
}
// 从每个相关数据库中查询消息
totalMessages := []*model.Message{}
for _, dbInfo := range dbInfos {
// 检查上下文是否已取消
if err := ctx.Err(); err != nil {
return nil, err
}
db, ok := ds.messageDbs[dbInfo.FilePath]
if !ok {
log.Printf("警告: 数据库 %s 未打开", dbInfo.FilePath)
continue
}
messages, err := ds.getMessagesFromDB(ctx, db, dbInfo, startTime, endTime, talker)
if err != nil {
log.Printf("警告: 从数据库 %s 获取消息失败: %v", dbInfo.FilePath, err)
continue
}
totalMessages = append(totalMessages, messages...)
if limit+offset > 0 && len(totalMessages) >= limit+offset {
break
}
}
// 对所有消息按时间排序
sort.Slice(totalMessages, func(i, j int) bool {
return totalMessages[i].Sequence < totalMessages[j].Sequence
})
// 处理分页
if limit > 0 {
if offset >= len(totalMessages) {
return []*model.Message{}, nil
}
end := offset + limit
if end > len(totalMessages) {
end = len(totalMessages)
}
return totalMessages[offset:end], nil
}
return totalMessages, nil
}
// getMessagesSingleFile 从单个数据库文件获取消息
func (ds *DataSource) getMessagesSingleFile(ctx context.Context, dbInfo MessageDBInfo, startTime, endTime time.Time, talker string, limit, offset int) ([]*model.Message, error) {
db, ok := ds.messageDbs[dbInfo.FilePath]
if !ok {
return nil, fmt.Errorf("数据库 %s 未打开", dbInfo.FilePath)
}
// 构建表名
_talkerMd5Bytes := md5.Sum([]byte(talker))
talkerMd5 := hex.EncodeToString(_talkerMd5Bytes[:])
tableName := "Msg_" + talkerMd5
// 构建查询条件
conditions := []string{"create_time >= ? AND create_time <= ?"}
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
WHERE %s
ORDER BY sort_seq ASC
`, tableName, strings.Join(conditions, " AND "))
if limit > 0 {
query += fmt.Sprintf(" LIMIT %d", limit)
if offset > 0 {
query += fmt.Sprintf(" OFFSET %d", offset)
}
}
// 执行查询
rows, err := db.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("查询数据库 %s 失败: %w", dbInfo.FilePath, err)
}
defer rows.Close()
// 处理查询结果
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.CreateTime,
&msg.MessageContent,
&msg.PackedInfoData,
&msg.Status,
)
if err != nil {
return nil, fmt.Errorf("扫描消息行失败: %w", err)
}
messages = append(messages, msg.Wrap(dbInfo.ID2Name, isChatRoom))
}
return messages, nil
}
// getMessagesFromDB 从数据库获取消息
func (ds *DataSource) getMessagesFromDB(ctx context.Context, db *sql.DB, dbInfo MessageDBInfo, startTime, endTime time.Time, talker string) ([]*model.Message, error) {
// 构建表名
_talkerMd5Bytes := md5.Sum([]byte(talker))
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, fmt.Errorf("检查表 %s 是否存在失败: %w", tableName, err)
}
// 构建查询条件
conditions := []string{"create_time >= ? AND create_time <= ?"}
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
WHERE %s
ORDER BY sort_seq ASC
`, tableName, strings.Join(conditions, " AND "))
// 执行查询
rows, err := db.QueryContext(ctx, query, args...)
if err != nil {
// 如果表不存在SQLite 会返回错误
if strings.Contains(err.Error(), "no such table") {
return []*model.Message{}, nil
}
return nil, fmt.Errorf("查询数据库失败: %w", err)
}
defer rows.Close()
// 处理查询结果
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.CreateTime,
&msg.MessageContent,
&msg.PackedInfoData,
&msg.Status,
)
if err != nil {
return nil, fmt.Errorf("扫描消息行失败: %w", err)
}
messages = append(messages, msg.Wrap(dbInfo.ID2Name, isChatRoom))
}
return messages, nil
}
// 联系人
func (ds *DataSource) GetContacts(ctx context.Context, key string, limit, offset int) ([]*model.Contact, error) {
var query string
var args []interface{}
if key != "" {
// 按照关键字查询
query = `SELECT username, local_type, alias, remark, nick_name
FROM contact
WHERE username = ? OR alias = ? OR remark = ? OR nick_name = ?`
args = []interface{}{key, key, key, key}
} else {
// 查询所有联系人
query = `SELECT username, local_type, alias, remark, nick_name FROM contact`
}
// 添加排序、分页
query += ` ORDER BY username`
if limit > 0 {
query += fmt.Sprintf(" LIMIT %d", limit)
if offset > 0 {
query += fmt.Sprintf(" OFFSET %d", offset)
}
}
// 执行查询
rows, err := ds.contactDb.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("查询联系人失败: %w", err)
}
defer rows.Close()
contacts := []*model.Contact{}
for rows.Next() {
var contactV4 model.ContactV4
err := rows.Scan(
&contactV4.UserName,
&contactV4.LocalType,
&contactV4.Alias,
&contactV4.Remark,
&contactV4.NickName,
)
if err != nil {
return nil, fmt.Errorf("扫描联系人行失败: %w", err)
}
contacts = append(contacts, contactV4.Wrap())
}
return contacts, nil
}
// 群聊
func (ds *DataSource) GetChatRooms(ctx context.Context, key string, limit, offset int) ([]*model.ChatRoom, error) {
var query string
var args []interface{}
if key != "" {
// 按照关键字查询
query = `SELECT username, owner, ext_buffer FROM chat_room WHERE username = ?`
args = []interface{}{key}
// 执行查询
rows, err := ds.contactDb.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("查询群聊失败: %w", err)
}
defer rows.Close()
chatRooms := []*model.ChatRoom{}
for rows.Next() {
var chatRoomV4 model.ChatRoomV4
err := rows.Scan(
&chatRoomV4.UserName,
&chatRoomV4.Owner,
&chatRoomV4.ExtBuffer,
)
if err != nil {
return nil, fmt.Errorf("扫描群聊行失败: %w", err)
}
chatRooms = append(chatRooms, chatRoomV4.Wrap())
}
// 如果没有找到群聊,尝试通过联系人查找
if len(chatRooms) == 0 {
contacts, err := ds.GetContacts(ctx, key, 1, 0)
if err == nil && len(contacts) > 0 && strings.HasSuffix(contacts[0].UserName, "@chatroom") {
// 再次尝试通过用户名查找群聊
rows, err := ds.contactDb.QueryContext(ctx,
`SELECT username, owner, ext_buffer FROM chat_room WHERE username = ?`,
contacts[0].UserName)
if err != nil {
return nil, fmt.Errorf("查询群聊失败: %w", err)
}
defer rows.Close()
for rows.Next() {
var chatRoomV4 model.ChatRoomV4
err := rows.Scan(
&chatRoomV4.UserName,
&chatRoomV4.Owner,
&chatRoomV4.ExtBuffer,
)
if err != nil {
return nil, fmt.Errorf("扫描群聊行失败: %w", err)
}
chatRooms = append(chatRooms, chatRoomV4.Wrap())
}
// 如果群聊记录不存在,但联系人记录存在,创建一个模拟的群聊对象
if len(chatRooms) == 0 {
chatRooms = append(chatRooms, &model.ChatRoom{
Name: contacts[0].UserName,
Users: make([]model.ChatRoomUser, 0),
User2DisplayName: make(map[string]string),
})
}
}
}
return chatRooms, nil
} else {
// 查询所有群聊
query = `SELECT username, owner, ext_buffer FROM chat_room`
// 添加排序、分页
query += ` ORDER BY username`
if limit > 0 {
query += fmt.Sprintf(" LIMIT %d", limit)
if offset > 0 {
query += fmt.Sprintf(" OFFSET %d", offset)
}
}
// 执行查询
rows, err := ds.contactDb.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("查询群聊失败: %w", err)
}
defer rows.Close()
chatRooms := []*model.ChatRoom{}
for rows.Next() {
var chatRoomV4 model.ChatRoomV4
err := rows.Scan(
&chatRoomV4.UserName,
&chatRoomV4.Owner,
&chatRoomV4.ExtBuffer,
)
if err != nil {
return nil, fmt.Errorf("扫描群聊行失败: %w", err)
}
chatRooms = append(chatRooms, chatRoomV4.Wrap())
}
return chatRooms, nil
}
}
// 最近会话
func (ds *DataSource) GetSessions(ctx context.Context, key string, limit, offset int) ([]*model.Session, error) {
var query string
var args []interface{}
if key != "" {
// 按照关键字查询
query = `SELECT username, summary, last_timestamp, last_msg_sender, last_sender_display_name
FROM SessionTable
WHERE username = ? OR last_sender_display_name = ?
ORDER BY sort_timestamp DESC`
args = []interface{}{key, key}
} else {
// 查询所有会话
query = `SELECT username, summary, last_timestamp, last_msg_sender, last_sender_display_name
FROM SessionTable
ORDER BY sort_timestamp DESC`
}
// 添加分页
if limit > 0 {
query += fmt.Sprintf(" LIMIT %d", limit)
if offset > 0 {
query += fmt.Sprintf(" OFFSET %d", offset)
}
}
// 执行查询
rows, err := ds.sessionDb.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("查询会话失败: %w", err)
}
defer rows.Close()
sessions := []*model.Session{}
for rows.Next() {
var sessionV4 model.SessionV4
err := rows.Scan(
&sessionV4.Username,
&sessionV4.Summary,
&sessionV4.LastTimestamp,
&sessionV4.LastMsgSender,
&sessionV4.LastSenderDisplayName,
)
if err != nil {
return nil, fmt.Errorf("扫描会话行失败: %w", err)
}
sessions = append(sessions, sessionV4.Wrap())
}
return sessions, nil
}
func (ds *DataSource) Close() error {
var errs []error
// 关闭消息数据库连接
for path, db := range ds.messageDbs {
if err := db.Close(); err != nil {
errs = append(errs, fmt.Errorf("关闭消息数据库 %s 失败: %w", path, err))
}
}
// 关闭联系人数据库连接
if ds.contactDb != nil {
if err := ds.contactDb.Close(); err != nil {
errs = append(errs, fmt.Errorf("关闭联系人数据库失败: %w", err))
}
}
// 关闭会话数据库连接
if ds.sessionDb != nil {
if err := ds.sessionDb.Close(); err != nil {
errs = append(errs, fmt.Errorf("关闭会话数据库失败: %w", err))
}
}
if len(errs) > 0 {
return fmt.Errorf("关闭数据库连接时发生错误: %v", errs)
}
return nil
}

View File

@@ -0,0 +1,615 @@
package windowsv3
import (
"context"
"database/sql"
"fmt"
"log"
"sort"
"strings"
"time"
"github.com/sjzar/chatlog/internal/model"
"github.com/sjzar/chatlog/pkg/util"
_ "github.com/mattn/go-sqlite3"
)
const (
MessageFilePattern = "^MSG([0-9]?[0-9])?\\.db$"
ContactFilePattern = "^MicroMsg.db$"
)
// MessageDBInfo 保存消息数据库的信息
type MessageDBInfo struct {
FilePath string
StartTime time.Time
EndTime time.Time
TalkerMap map[string]int
}
// DataSource 实现了 DataSource 接口
type DataSource struct {
// 消息数据库
messageFiles []MessageDBInfo
messageDbs map[string]*sql.DB
// 联系人数据库
contactDbFile string
contactDb *sql.DB
}
// New 创建一个新的 WindowsV3DataSource
func New(path string) (*DataSource, error) {
ds := &DataSource{
messageFiles: make([]MessageDBInfo, 0),
messageDbs: make(map[string]*sql.DB),
}
// 初始化消息数据库
if err := ds.initMessageDbs(path); err != nil {
return nil, fmt.Errorf("初始化消息数据库失败: %w", err)
}
// 初始化联系人数据库
if err := ds.initContactDb(path); err != nil {
return nil, fmt.Errorf("初始化联系人数据库失败: %w", err)
}
return ds, nil
}
// initMessageDbs 初始化消息数据库
func (ds *DataSource) initMessageDbs(path string) error {
// 查找所有消息数据库文件
files, err := util.FindFilesWithPatterns(path, MessageFilePattern, true)
if err != nil {
return fmt.Errorf("查找消息数据库文件失败: %w", err)
}
if len(files) == 0 {
return fmt.Errorf("未找到任何消息数据库文件: %s", path)
}
// 处理每个数据库文件
for _, filePath := range files {
// 连接数据库
db, err := sql.Open("sqlite3", filePath)
if err != nil {
log.Printf("警告: 连接数据库 %s 失败: %v", filePath, err)
continue
}
// 获取 DBInfo 表中的开始时间
var startTime time.Time
rows, err := db.Query("SELECT tableIndex, tableVersion, tableDesc FROM DBInfo")
if err != nil {
log.Printf("警告: 查询数据库 %s 的 DBInfo 表失败: %v", filePath, err)
db.Close()
continue
}
for rows.Next() {
var tableIndex int
var tableVersion int64
var tableDesc string
if err := rows.Scan(&tableIndex, &tableVersion, &tableDesc); err != nil {
log.Printf("警告: 扫描 DBInfo 行失败: %v", err)
continue
}
// 查找描述为 "Start Time" 的记录
if strings.Contains(tableDesc, "Start Time") {
startTime = time.Unix(tableVersion/1000, (tableVersion%1000)*1000000)
break
}
}
rows.Close()
// 组织 TalkerMap
talkerMap := make(map[string]int)
rows, err = db.Query("SELECT UsrName FROM Name2ID")
if err != nil {
log.Printf("警告: 查询数据库 %s 的 Name2ID 表失败: %v", filePath, err)
db.Close()
continue
}
i := 1
for rows.Next() {
var userName string
if err := rows.Scan(&userName); err != nil {
log.Printf("警告: 扫描 Name2ID 行失败: %v", err)
continue
}
talkerMap[userName] = i
i++
}
rows.Close()
// 保存数据库信息
ds.messageFiles = append(ds.messageFiles, MessageDBInfo{
FilePath: filePath,
StartTime: startTime,
TalkerMap: talkerMap,
})
// 保存数据库连接
ds.messageDbs[filePath] = db
}
// 按照 StartTime 排序数据库文件
sort.Slice(ds.messageFiles, func(i, j int) bool {
return ds.messageFiles[i].StartTime.Before(ds.messageFiles[j].StartTime)
})
// 设置结束时间
for i := range ds.messageFiles {
if i == len(ds.messageFiles)-1 {
ds.messageFiles[i].EndTime = time.Now()
} else {
ds.messageFiles[i].EndTime = ds.messageFiles[i+1].StartTime
}
}
return nil
}
// initContactDb 初始化联系人数据库
func (ds *DataSource) initContactDb(path string) error {
files, err := util.FindFilesWithPatterns(path, ContactFilePattern, true)
if err != nil {
return fmt.Errorf("查找联系人数据库文件失败: %w", err)
}
if len(files) == 0 {
return fmt.Errorf("未找到联系人数据库文件: %s", path)
}
ds.contactDbFile = files[0]
ds.contactDb, err = sql.Open("sqlite3", ds.contactDbFile)
if err != nil {
return fmt.Errorf("连接联系人数据库失败: %w", err)
}
return nil
}
// getDBInfosForTimeRange 获取时间范围内的数据库信息
func (ds *DataSource) getDBInfosForTimeRange(startTime, endTime time.Time) []MessageDBInfo {
var dbs []MessageDBInfo
for _, info := range ds.messageFiles {
if info.StartTime.Before(endTime) && info.EndTime.After(startTime) {
dbs = append(dbs, info)
}
}
return dbs
}
// GetMessages 实现 DataSource 接口的 GetMessages 方法
func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.Time, talker string, limit, offset int) ([]*model.Message, error) {
// 找到时间范围内的数据库文件
dbInfos := ds.getDBInfosForTimeRange(startTime, endTime)
if len(dbInfos) == 0 {
return nil, fmt.Errorf("未找到时间范围 %v 到 %v 内的数据库文件", startTime, endTime)
}
if len(dbInfos) == 1 {
// LIMIT 和 OFFSET 逻辑在单文件情况下可以直接在 SQL 里处理
return ds.getMessagesSingleFile(ctx, dbInfos[0], startTime, endTime, talker, limit, offset)
}
// 从每个相关数据库中查询消息
totalMessages := []*model.Message{}
for _, dbInfo := range dbInfos {
// 检查上下文是否已取消
if err := ctx.Err(); err != nil {
return nil, err
}
db, ok := ds.messageDbs[dbInfo.FilePath]
if !ok {
log.Printf("警告: 数据库 %s 未打开", dbInfo.FilePath)
continue
}
// 构建查询条件
conditions := []string{"Sequence >= ? AND Sequence <= ?"}
args := []interface{}{startTime.Unix() * 1000, endTime.Unix() * 1000}
if len(talker) > 0 {
talkerID, ok := dbInfo.TalkerMap[talker]
if ok {
conditions = append(conditions, "TalkerId = ?")
args = append(args, talkerID)
} else {
conditions = append(conditions, "StrTalker = ?")
args = append(args, talker)
}
}
query := fmt.Sprintf(`
SELECT Sequence, CreateTime, TalkerId, StrTalker, IsSender,
Type, SubType, StrContent, CompressContent, BytesExtra
FROM MSG
WHERE %s
ORDER BY Sequence ASC
`, strings.Join(conditions, " AND "))
// 执行查询
rows, err := db.QueryContext(ctx, query, args...)
if err != nil {
log.Printf("警告: 查询数据库 %s 失败: %v", dbInfo.FilePath, err)
continue
}
// 处理查询结果
for rows.Next() {
var msg model.MessageV3
var compressContent []byte
var bytesExtra []byte
err := rows.Scan(
&msg.Sequence,
&msg.CreateTime,
&msg.TalkerID,
&msg.StrTalker,
&msg.IsSender,
&msg.Type,
&msg.SubType,
&msg.StrContent,
&compressContent,
&bytesExtra,
)
if err != nil {
log.Printf("警告: 扫描消息行失败: %v", err)
continue
}
msg.CompressContent = compressContent
msg.BytesExtra = bytesExtra
totalMessages = append(totalMessages, msg.Wrap())
}
rows.Close()
if limit+offset > 0 && len(totalMessages) >= limit+offset {
break
}
}
// 对所有消息按时间排序
sort.Slice(totalMessages, func(i, j int) bool {
return totalMessages[i].Sequence < totalMessages[j].Sequence
})
// 处理分页
if limit > 0 {
if offset >= len(totalMessages) {
return []*model.Message{}, nil
}
end := offset + limit
if end > len(totalMessages) {
end = len(totalMessages)
}
return totalMessages[offset:end], nil
}
return totalMessages, nil
}
// getMessagesSingleFile 从单个数据库文件获取消息
func (ds *DataSource) getMessagesSingleFile(ctx context.Context, dbInfo MessageDBInfo, startTime, endTime time.Time, talker string, limit, offset int) ([]*model.Message, error) {
// 构建查询条件
conditions := []string{"Sequence >= ? AND Sequence <= ?"}
args := []interface{}{startTime.Unix() * 1000, endTime.Unix() * 1000}
if len(talker) > 0 {
// TalkerId 有索引,优先使用
talkerID, ok := dbInfo.TalkerMap[talker]
if ok {
conditions = append(conditions, "TalkerId = ?")
args = append(args, talkerID)
} else {
conditions = append(conditions, "StrTalker = ?")
args = append(args, talker)
}
}
query := fmt.Sprintf(`
SELECT Sequence, CreateTime, TalkerId, StrTalker, IsSender,
Type, SubType, StrContent, CompressContent, BytesExtra
FROM MSG
WHERE %s
ORDER BY Sequence ASC
`, strings.Join(conditions, " AND "))
if limit > 0 {
query += fmt.Sprintf(" LIMIT %d", limit)
if offset > 0 {
query += fmt.Sprintf(" OFFSET %d", offset)
}
}
// 执行查询
rows, err := ds.messageDbs[dbInfo.FilePath].QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("查询数据库 %s 失败: %w", dbInfo.FilePath, err)
}
defer rows.Close()
// 处理查询结果
totalMessages := []*model.Message{}
for rows.Next() {
var msg model.MessageV3
var compressContent []byte
var bytesExtra []byte
err := rows.Scan(
&msg.Sequence,
&msg.CreateTime,
&msg.TalkerID,
&msg.StrTalker,
&msg.IsSender,
&msg.Type,
&msg.SubType,
&msg.StrContent,
&compressContent,
&bytesExtra,
)
if err != nil {
return nil, fmt.Errorf("扫描消息行失败: %w", err)
}
msg.CompressContent = compressContent
msg.BytesExtra = bytesExtra
totalMessages = append(totalMessages, msg.Wrap())
}
return totalMessages, nil
}
// GetContacts 实现获取联系人信息的方法
func (ds *DataSource) GetContacts(ctx context.Context, key string, limit, offset int) ([]*model.Contact, error) {
var query string
var args []interface{}
if key != "" {
// 按照关键字查询
query = `SELECT UserName, Alias, Remark, NickName, Reserved1 FROM Contact
WHERE UserName = ? OR Alias = ? OR Remark = ? OR NickName = ?`
args = []interface{}{key, key, key, key}
} else {
// 查询所有联系人
query = `SELECT UserName, Alias, Remark, NickName, Reserved1 FROM Contact`
}
// 添加排序、分页
query += ` ORDER BY UserName`
if limit > 0 {
query += fmt.Sprintf(" LIMIT %d", limit)
if offset > 0 {
query += fmt.Sprintf(" OFFSET %d", offset)
}
}
// 执行查询
rows, err := ds.contactDb.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("查询联系人失败: %w", err)
}
defer rows.Close()
contacts := []*model.Contact{}
for rows.Next() {
var contactV3 model.ContactV3
err := rows.Scan(
&contactV3.UserName,
&contactV3.Alias,
&contactV3.Remark,
&contactV3.NickName,
&contactV3.Reserved1,
)
if err != nil {
return nil, fmt.Errorf("扫描联系人行失败: %w", err)
}
contacts = append(contacts, contactV3.Wrap())
}
return contacts, nil
}
// GetChatRooms 实现获取群聊信息的方法
func (ds *DataSource) GetChatRooms(ctx context.Context, key string, limit, offset int) ([]*model.ChatRoom, error) {
var query string
var args []interface{}
if key != "" {
// 按照关键字查询
query = `SELECT ChatRoomName, Reserved2, RoomData FROM ChatRoom WHERE ChatRoomName = ?`
args = []interface{}{key}
// 执行查询
rows, err := ds.contactDb.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("查询群聊失败: %w", err)
}
defer rows.Close()
chatRooms := []*model.ChatRoom{}
for rows.Next() {
var chatRoomV3 model.ChatRoomV3
err := rows.Scan(
&chatRoomV3.ChatRoomName,
&chatRoomV3.Reserved2,
&chatRoomV3.RoomData,
)
if err != nil {
return nil, fmt.Errorf("扫描群聊行失败: %w", err)
}
chatRooms = append(chatRooms, chatRoomV3.Wrap())
}
// 如果没有找到群聊,尝试通过联系人查找
if len(chatRooms) == 0 {
contacts, err := ds.GetContacts(ctx, key, 1, 0)
if err == nil && len(contacts) > 0 && strings.HasSuffix(contacts[0].UserName, "@chatroom") {
// 再次尝试通过用户名查找群聊
rows, err := ds.contactDb.QueryContext(ctx,
`SELECT ChatRoomName, Reserved2, RoomData FROM ChatRoom WHERE ChatRoomName = ?`,
contacts[0].UserName)
if err != nil {
return nil, fmt.Errorf("查询群聊失败: %w", err)
}
defer rows.Close()
for rows.Next() {
var chatRoomV3 model.ChatRoomV3
err := rows.Scan(
&chatRoomV3.ChatRoomName,
&chatRoomV3.Reserved2,
&chatRoomV3.RoomData,
)
if err != nil {
return nil, fmt.Errorf("扫描群聊行失败: %w", err)
}
chatRooms = append(chatRooms, chatRoomV3.Wrap())
}
// 如果群聊记录不存在,但联系人记录存在,创建一个模拟的群聊对象
if len(chatRooms) == 0 {
chatRooms = append(chatRooms, &model.ChatRoom{
Name: contacts[0].UserName,
Users: make([]model.ChatRoomUser, 0),
User2DisplayName: make(map[string]string),
})
}
}
}
return chatRooms, nil
} else {
// 查询所有群聊
query = `SELECT ChatRoomName, Reserved2, RoomData FROM ChatRoom`
// 添加排序、分页
query += ` ORDER BY ChatRoomName`
if limit > 0 {
query += fmt.Sprintf(" LIMIT %d", limit)
if offset > 0 {
query += fmt.Sprintf(" OFFSET %d", offset)
}
}
// 执行查询
rows, err := ds.contactDb.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("查询群聊失败: %w", err)
}
defer rows.Close()
chatRooms := []*model.ChatRoom{}
for rows.Next() {
var chatRoomV3 model.ChatRoomV3
err := rows.Scan(
&chatRoomV3.ChatRoomName,
&chatRoomV3.Reserved2,
&chatRoomV3.RoomData,
)
if err != nil {
return nil, fmt.Errorf("扫描群聊行失败: %w", err)
}
chatRooms = append(chatRooms, chatRoomV3.Wrap())
}
return chatRooms, nil
}
}
// GetSessions 实现获取会话信息的方法
func (ds *DataSource) GetSessions(ctx context.Context, key string, limit, offset int) ([]*model.Session, error) {
var query string
var args []interface{}
if key != "" {
// 按照关键字查询
query = `SELECT strUsrName, nOrder, strNickName, strContent, nTime
FROM Session
WHERE strUsrName = ? OR strNickName = ?
ORDER BY nOrder DESC`
args = []interface{}{key, key}
} else {
// 查询所有会话
query = `SELECT strUsrName, nOrder, strNickName, strContent, nTime
FROM Session
ORDER BY nOrder DESC`
}
// 添加分页
if limit > 0 {
query += fmt.Sprintf(" LIMIT %d", limit)
if offset > 0 {
query += fmt.Sprintf(" OFFSET %d", offset)
}
}
// 执行查询
rows, err := ds.contactDb.QueryContext(ctx, query, args...)
if err != nil {
return nil, fmt.Errorf("查询会话失败: %w", err)
}
defer rows.Close()
sessions := []*model.Session{}
for rows.Next() {
var sessionV3 model.SessionV3
err := rows.Scan(
&sessionV3.StrUsrName,
&sessionV3.NOrder,
&sessionV3.StrNickName,
&sessionV3.StrContent,
&sessionV3.NTime,
)
if err != nil {
return nil, fmt.Errorf("扫描会话行失败: %w", err)
}
sessions = append(sessions, sessionV3.Wrap())
}
return sessions, nil
}
// Close 实现 DataSource 接口的 Close 方法
func (ds *DataSource) Close() error {
var errs []error
// 关闭消息数据库连接
for path, db := range ds.messageDbs {
if err := db.Close(); err != nil {
errs = append(errs, fmt.Errorf("关闭消息数据库 %s 失败: %w", path, err))
}
}
// 关闭联系人数据库连接
if ds.contactDb != nil {
if err := ds.contactDb.Close(); err != nil {
errs = append(errs, fmt.Errorf("关闭联系人数据库失败: %w", err))
}
}
if len(errs) > 0 {
return fmt.Errorf("关闭数据库连接时发生错误: %v", errs)
}
return nil
}

View File

@@ -1,321 +0,0 @@
package wechatdb
import (
"database/sql"
"fmt"
"log"
"sort"
"strings"
"time"
"github.com/sjzar/chatlog/pkg/model"
"github.com/sjzar/chatlog/pkg/util"
_ "github.com/mattn/go-sqlite3"
)
const (
MessageFileV3 = "^MSG([0-9]?[0-9])?\\.db$"
MessageFileV4 = "^messages_([0-9]?[0-9])+\\.db$"
)
type Message struct {
version int
files []MsgDBInfo
dbs map[string]*sql.DB
}
type MsgDBInfo struct {
FilePath string
StartTime time.Time
EndTime time.Time
TalkerMap map[string]int
}
func NewMessage(path string, version int) (*Message, error) {
m := &Message{
version: version,
files: make([]MsgDBInfo, 0),
dbs: make(map[string]*sql.DB),
}
// 查找所有 MSG[0-13].db 文件
files, err := util.FindFilesWithPatterns(path, MessageFileV3, true)
if err != nil {
return nil, fmt.Errorf("查找数据库文件失败: %v", err)
}
if len(files) == 0 {
return nil, fmt.Errorf("未找到任何数据库文件: %s", path)
}
// 处理每个数据库文件
for _, filePath := range files {
// 连接数据库
db, err := sql.Open("sqlite3", filePath)
if err != nil {
log.Printf("警告: 连接数据库 %s 失败: %v", filePath, err)
continue
}
// 获取 DBInfo 表中的开始时间
// 首先检查表结构
var startTime time.Time
// 尝试从 DBInfo 表中查找 Start Time 对应的记录
rows, err := db.Query("SELECT tableIndex, tableVersion, tableDesc FROM DBInfo")
if err != nil {
log.Printf("警告: 查询数据库 %s 的 DBInfo 表失败: %v", filePath, err)
db.Close()
continue
}
for rows.Next() {
var tableIndex int
var tableVersion int64
var tableDesc string
if err := rows.Scan(&tableIndex, &tableVersion, &tableDesc); err != nil {
log.Printf("警告: 扫描 DBInfo 行失败: %v", err)
continue
}
// 查找描述为 "Start Time" 的记录
if strings.Contains(tableDesc, "Start Time") {
startTime = time.Unix(tableVersion/1000, (tableVersion%1000)*1000000)
break
}
}
rows.Close()
// 组织 TalkerMap
talkerMap := make(map[string]int)
rows, err = db.Query("SELECT UsrName FROM Name2ID")
if err != nil {
log.Printf("警告: 查询数据库 %s 的 Name2ID 表失败: %v", filePath, err)
db.Close()
continue
}
i := 1
for rows.Next() {
var userName string
if err := rows.Scan(&userName); err != nil {
log.Printf("警告: 扫描 Name2ID 行失败: %v", err)
continue
}
talkerMap[userName] = i
i++
}
// 保存数据库信息
m.files = append(m.files, MsgDBInfo{
FilePath: filePath,
StartTime: startTime,
TalkerMap: talkerMap,
})
// 保存数据库连接
m.dbs[filePath] = db
}
// 按照 StartTime 排序数据库文件
sort.Slice(m.files, func(i, j int) bool {
return m.files[i].StartTime.Before(m.files[j].StartTime)
})
for i := range m.files {
if i == len(m.files)-1 {
m.files[i].EndTime = time.Now()
} else {
m.files[i].EndTime = m.files[i+1].StartTime
}
}
return m, nil
}
// GetMessages 根据时间段和 talker 查询聊天记录
func (m *Message) GetMessages(startTime, endTime time.Time, talker string, limit, offset int) ([]*model.Message, error) {
// 找到时间范围内的数据库文件
dbInfos := m.getDBInfosForTimeRange(startTime, endTime)
if len(dbInfos) == 0 {
return nil, fmt.Errorf("未找到时间范围 %v 到 %v 内的数据库文件", startTime, endTime)
}
if len(dbInfos) == 1 {
// LIMIT 和 OFFSET 逻辑在单文件情况下可以直接在 SQL 里处理
return m.getMessagesSingleFile(dbInfos[0], startTime, endTime, talker, limit, offset)
}
// 从每个相关数据库中查询消息
totalMessages := []*model.Message{}
for _, dbInfo := range dbInfos {
db, ok := m.dbs[dbInfo.FilePath]
if !ok {
log.Printf("警告: 数据库 %s 未打开", dbInfo.FilePath)
continue
}
// 构建查询条件
// 使用 Sequence 查询,有索引
conditions := []string{"Sequence >= ? AND Sequence <= ?"}
args := []interface{}{startTime.Unix() * 1000, endTime.Unix() * 1000}
if len(talker) > 0 {
talkerID, ok := dbInfo.TalkerMap[talker]
if ok {
conditions = append(conditions, "TalkerId = ?")
args = append(args, talkerID)
} else {
conditions = append(conditions, "StrTalker = ?")
args = append(args, talker)
}
}
query := fmt.Sprintf(`
SELECT Sequence, CreateTime, TalkerId, StrTalker, IsSender,
Type, SubType, StrContent, CompressContent, BytesExtra
FROM MSG
WHERE %s
ORDER BY Sequence ASC
`, strings.Join(conditions, " AND "))
// 执行查询
rows, err := db.Query(query, args...)
if err != nil {
log.Printf("警告: 查询数据库 %s 失败: %v", dbInfo.FilePath, err)
continue
}
// 处理查询结果
for rows.Next() {
var msg model.MessageV3
var compressContent []byte
var bytesExtra []byte
err := rows.Scan(
&msg.Sequence,
&msg.CreateTime,
&msg.TalkerID,
&msg.StrTalker,
&msg.IsSender,
&msg.Type,
&msg.SubType,
&msg.StrContent,
&compressContent,
&bytesExtra,
)
if err != nil {
log.Printf("警告: 扫描消息行失败: %v", err)
continue
}
msg.CompressContent = compressContent
msg.BytesExtra = bytesExtra
totalMessages = append(totalMessages, msg.Wrap())
}
rows.Close()
if limit+offset > 0 && len(totalMessages) >= limit+offset {
break
}
}
// 对所有消息按时间排序
sort.Slice(totalMessages, func(i, j int) bool {
return totalMessages[i].Sequence < totalMessages[j].Sequence
})
// FIXME limit 和 offset 逻辑,在多文件边界条件下不好处理,直接查询全量数据后在进程里处理
if limit > 0 {
if offset >= len(totalMessages) {
return []*model.Message{}, nil
}
end := offset + limit
if end > len(totalMessages) || limit == 0 {
end = len(totalMessages)
}
return totalMessages[offset:end], nil
}
return totalMessages, nil
}
func (m *Message) getMessagesSingleFile(dbInfo MsgDBInfo, startTime, endTime time.Time, talker string, limit, offset int) ([]*model.Message, error) {
// 构建查询条件
// 使用 Sequence 查询,有索引
conditions := []string{"Sequence >= ? AND Sequence <= ?"}
args := []interface{}{startTime.Unix() * 1000, endTime.Unix() * 1000}
if len(talker) > 0 {
// TalkerId 有索引,优先使用
talkerID, ok := dbInfo.TalkerMap[talker]
if ok {
conditions = append(conditions, "TalkerId = ?")
args = append(args, talkerID)
} else {
conditions = append(conditions, "StrTalker = ?")
args = append(args, talker)
}
}
query := fmt.Sprintf(`
SELECT Sequence, CreateTime, TalkerId, StrTalker, IsSender,
Type, SubType, StrContent, CompressContent, BytesExtra
FROM MSG
WHERE %s
ORDER BY Sequence ASC
`, strings.Join(conditions, " AND "))
if limit > 0 {
query += fmt.Sprintf(" LIMIT %d", limit)
if offset > 0 {
query += fmt.Sprintf(" OFFSET %d", offset)
}
}
// 执行查询
rows, err := m.dbs[dbInfo.FilePath].Query(query, args...)
if err != nil {
return nil, fmt.Errorf("查询数据库 %s 失败: %v", dbInfo.FilePath, err)
}
defer rows.Close()
// 处理查询结果
totalMessages := []*model.Message{}
for rows.Next() {
var msg model.MessageV3
var compressContent []byte
var bytesExtra []byte
err := rows.Scan(
&msg.Sequence,
&msg.CreateTime,
&msg.TalkerID,
&msg.StrTalker,
&msg.IsSender,
&msg.Type,
&msg.SubType,
&msg.StrContent,
&compressContent,
&bytesExtra,
)
if err != nil {
return nil, fmt.Errorf("扫描消息行失败: %v", err)
}
msg.CompressContent = compressContent
msg.BytesExtra = bytesExtra
totalMessages = append(totalMessages, msg.Wrap())
}
return totalMessages, nil
}
func (m *Message) getDBInfosForTimeRange(startTime, endTime time.Time) []MsgDBInfo {
var dbs []MsgDBInfo
for _, info := range m.files {
if info.StartTime.Before(endTime) && info.EndTime.After(startTime) {
dbs = append(dbs, info)
}
}
return dbs
}

View File

@@ -0,0 +1,184 @@
package repository
import (
"context"
"fmt"
"sort"
"strings"
"github.com/sjzar/chatlog/internal/model"
)
// initChatRoomCache 初始化群聊缓存
func (r *Repository) initChatRoomCache(ctx context.Context) error {
// 加载所有群聊到缓存
chatRooms, err := r.ds.GetChatRooms(ctx, "", 0, 0)
if err != nil {
return fmt.Errorf("加载群聊失败: %w", err)
}
chatRoomMap := make(map[string]*model.ChatRoom)
remarkToChatRoom := make(map[string]*model.ChatRoom)
nickNameToChatRoom := make(map[string]*model.ChatRoom)
chatRoomList := make([]string, 0)
chatRoomRemark := make([]string, 0)
chatRoomNickName := make([]string, 0)
for _, chatRoom := range chatRooms {
// 补充群聊信息(从联系人中获取 Remark 和 NickName
r.enrichChatRoom(chatRoom)
chatRoomMap[chatRoom.Name] = chatRoom
chatRoomList = append(chatRoomList, chatRoom.Name)
if chatRoom.Remark != "" {
remarkToChatRoom[chatRoom.Remark] = chatRoom
chatRoomRemark = append(chatRoomRemark, chatRoom.Remark)
}
if chatRoom.NickName != "" {
nickNameToChatRoom[chatRoom.NickName] = chatRoom
chatRoomNickName = append(chatRoomNickName, chatRoom.NickName)
}
}
for _, contact := range r.chatRoomInContact {
if _, ok := chatRoomMap[contact.UserName]; !ok {
chatRoom := &model.ChatRoom{
Name: contact.UserName,
Remark: contact.Remark,
NickName: contact.NickName,
}
chatRoomMap[contact.UserName] = chatRoom
chatRoomList = append(chatRoomList, contact.UserName)
if contact.Remark != "" {
remarkToChatRoom[contact.Remark] = chatRoom
chatRoomRemark = append(chatRoomRemark, contact.Remark)
}
if contact.NickName != "" {
nickNameToChatRoom[contact.NickName] = chatRoom
chatRoomNickName = append(chatRoomNickName, contact.NickName)
}
}
}
sort.Strings(chatRoomList)
sort.Strings(chatRoomRemark)
sort.Strings(chatRoomNickName)
r.chatRoomCache = chatRoomMap
r.chatRoomList = chatRoomList
r.remarkToChatRoom = remarkToChatRoom
r.nickNameToChatRoom = nickNameToChatRoom
return nil
}
func (r *Repository) GetChatRooms(ctx context.Context, key string, limit, offset int) ([]*model.ChatRoom, error) {
ret := make([]*model.ChatRoom, 0)
if key != "" {
ret = r.findChatRooms(key)
if len(ret) == 0 {
return nil, fmt.Errorf("未找到群聊: %s", key)
}
if limit > 0 {
end := offset + limit
if end > len(ret) {
end = len(ret)
}
if offset >= len(ret) {
return []*model.ChatRoom{}, nil
}
return ret[offset:end], nil
}
} else {
list := r.chatRoomList
if limit > 0 {
end := offset + limit
if end > len(list) {
end = len(list)
}
if offset >= len(list) {
return []*model.ChatRoom{}, nil
}
list = list[offset:end]
}
for _, name := range list {
ret = append(ret, r.chatRoomCache[name])
}
}
return ret, nil
}
func (r *Repository) GetChatRoom(ctx context.Context, key string) (*model.ChatRoom, error) {
chatRoom := r.findChatRoom(key)
if chatRoom == nil {
return nil, fmt.Errorf("未找到群聊: %s", key)
}
return chatRoom, nil
}
// enrichChatRoom 从联系人信息中补充群聊信息
func (r *Repository) enrichChatRoom(chatRoom *model.ChatRoom) {
if contact, ok := r.contactCache[chatRoom.Name]; ok {
chatRoom.Remark = contact.Remark
chatRoom.NickName = contact.NickName
}
}
func (r *Repository) findChatRoom(key string) *model.ChatRoom {
if chatRoom, ok := r.chatRoomCache[key]; ok {
return chatRoom
}
if chatRoom, ok := r.remarkToChatRoom[key]; ok {
return chatRoom
}
if chatRoom, ok := r.nickNameToChatRoom[key]; ok {
return chatRoom
}
// Contain
for _, remark := range r.chatRoomRemark {
if strings.Contains(remark, key) {
return r.remarkToChatRoom[remark]
}
}
for _, nickName := range r.chatRoomNickName {
if strings.Contains(nickName, key) {
return r.nickNameToChatRoom[nickName]
}
}
return nil
}
func (r *Repository) findChatRooms(key string) []*model.ChatRoom {
ret := make([]*model.ChatRoom, 0)
distinct := make(map[string]bool)
if chatRoom, ok := r.chatRoomCache[key]; ok {
ret = append(ret, chatRoom)
distinct[chatRoom.Name] = true
}
if chatRoom, ok := r.remarkToChatRoom[key]; ok && !distinct[chatRoom.Name] {
ret = append(ret, chatRoom)
distinct[chatRoom.Name] = true
}
if chatRoom, ok := r.nickNameToChatRoom[key]; ok && !distinct[chatRoom.Name] {
ret = append(ret, chatRoom)
distinct[chatRoom.Name] = true
}
// Contain
for _, remark := range r.chatRoomRemark {
if strings.Contains(remark, key) && !distinct[r.remarkToChatRoom[remark].Name] {
ret = append(ret, r.remarkToChatRoom[remark])
distinct[r.remarkToChatRoom[remark].Name] = true
}
}
for _, nickName := range r.chatRoomNickName {
if strings.Contains(nickName, key) && !distinct[r.nickNameToChatRoom[nickName].Name] {
ret = append(ret, r.nickNameToChatRoom[nickName])
distinct[r.nickNameToChatRoom[nickName].Name] = true
}
}
return ret
}

View File

@@ -0,0 +1,211 @@
package repository
import (
"context"
"fmt"
"sort"
"strings"
"github.com/sjzar/chatlog/internal/model"
)
// initContactCache 初始化联系人缓存
func (r *Repository) initContactCache(ctx context.Context) error {
// 加载所有联系人到缓存
contacts, err := r.ds.GetContacts(ctx, "", 0, 0)
if err != nil {
return fmt.Errorf("加载联系人失败: %w", err)
}
contactMap := make(map[string]*model.Contact)
aliasMap := make(map[string]*model.Contact)
remarkMap := make(map[string]*model.Contact)
nickNameMap := make(map[string]*model.Contact)
chatRoomUserMap := make(map[string]*model.Contact)
chatRoomInContactMap := make(map[string]*model.Contact)
contactList := make([]string, 0)
aliasList := make([]string, 0)
remarkList := make([]string, 0)
nickNameList := make([]string, 0)
for _, contact := range contacts {
contactMap[contact.UserName] = contact
contactList = append(contactList, contact.UserName)
// 建立快速查找索引
if contact.Alias != "" {
aliasMap[contact.Alias] = contact
aliasList = append(aliasList, contact.Alias)
}
if contact.Remark != "" {
remarkMap[contact.Remark] = contact
remarkList = append(remarkList, contact.Remark)
}
if contact.NickName != "" {
nickNameMap[contact.NickName] = contact
nickNameList = append(nickNameList, contact.NickName)
}
// 如果是群聊成员(非好友),添加到群聊成员索引
if !contact.IsFriend {
chatRoomUserMap[contact.UserName] = contact
}
if strings.HasSuffix(contact.UserName, "@chatroom") {
chatRoomInContactMap[contact.UserName] = contact
}
}
sort.Strings(contactList)
sort.Strings(aliasList)
sort.Strings(remarkList)
sort.Strings(nickNameList)
r.contactCache = contactMap
r.aliasToContact = aliasMap
r.remarkToContact = remarkMap
r.nickNameToContact = nickNameMap
r.chatRoomUserToInfo = chatRoomUserMap
r.chatRoomInContact = chatRoomInContactMap
r.contactList = contactList
r.aliasList = aliasList
r.remarkList = remarkList
r.nickNameList = nickNameList
return nil
}
func (r *Repository) GetContact(ctx context.Context, key string) (*model.Contact, error) {
// 先尝试从缓存中获取
contact := r.findContact(key)
if contact == nil {
return nil, fmt.Errorf("未找到联系人: %s", key)
}
return contact, nil
}
func (r *Repository) GetContacts(ctx context.Context, key string, limit, offset int) ([]*model.Contact, error) {
ret := make([]*model.Contact, 0)
if key != "" {
ret = r.findContacts(key)
if len(ret) == 0 {
return nil, fmt.Errorf("未找到联系人: %s", key)
}
if limit > 0 {
end := offset + limit
if end > len(ret) {
end = len(ret)
}
if offset >= len(ret) {
return []*model.Contact{}, nil
}
return ret[offset:end], nil
}
} else {
list := r.contactList
if limit > 0 {
end := offset + limit
if end > len(list) {
end = len(list)
}
if offset >= len(list) {
return []*model.Contact{}, nil
}
list = list[offset:end]
}
for _, name := range list {
ret = append(ret, r.contactCache[name])
}
}
return ret, nil
}
func (r *Repository) findContact(key string) *model.Contact {
if contact, ok := r.contactCache[key]; ok {
return contact
}
if contact, ok := r.aliasToContact[key]; ok {
return contact
}
if contact, ok := r.remarkToContact[key]; ok {
return contact
}
if contact, ok := r.nickNameToContact[key]; ok {
return contact
}
// Contain
for _, alias := range r.aliasList {
if strings.Contains(alias, key) {
return r.aliasToContact[alias]
}
}
for _, remark := range r.remarkList {
if strings.Contains(remark, key) {
return r.remarkToContact[remark]
}
}
for _, nickName := range r.nickNameList {
if strings.Contains(nickName, key) {
return r.nickNameToContact[nickName]
}
}
return nil
}
func (r *Repository) findContacts(key string) []*model.Contact {
ret := make([]*model.Contact, 0)
distinct := make(map[string]bool)
if contact, ok := r.contactCache[key]; ok {
ret = append(ret, contact)
distinct[contact.UserName] = true
}
if contact, ok := r.aliasToContact[key]; ok && !distinct[contact.UserName] {
ret = append(ret, contact)
distinct[contact.UserName] = true
}
if contact, ok := r.remarkToContact[key]; ok && !distinct[contact.UserName] {
ret = append(ret, contact)
distinct[contact.UserName] = true
}
if contact, ok := r.nickNameToContact[key]; ok && !distinct[contact.UserName] {
ret = append(ret, contact)
distinct[contact.UserName] = true
}
// Contain
for _, alias := range r.aliasList {
if strings.Contains(alias, key) && !distinct[r.aliasToContact[alias].UserName] {
ret = append(ret, r.aliasToContact[alias])
distinct[r.aliasToContact[alias].UserName] = true
}
}
for _, remark := range r.remarkList {
if strings.Contains(remark, key) && !distinct[r.remarkToContact[remark].UserName] {
ret = append(ret, r.remarkToContact[remark])
distinct[r.remarkToContact[remark].UserName] = true
}
}
for _, nickName := range r.nickNameList {
if strings.Contains(nickName, key) && !distinct[r.nickNameToContact[nickName].UserName] {
ret = append(ret, r.nickNameToContact[nickName])
distinct[r.nickNameToContact[nickName].UserName] = true
}
}
return ret
}
// getFullContact 获取联系人信息,包括群聊成员
func (r *Repository) getFullContact(userName string) *model.Contact {
// 先查找联系人缓存
if contact, ok := r.contactCache[userName]; ok {
return contact
}
// 再查找群聊成员缓存
contact, ok := r.chatRoomUserToInfo[userName]
if ok {
return contact
}
return nil
}

View File

@@ -0,0 +1,68 @@
package repository
import (
"context"
"time"
"github.com/sjzar/chatlog/internal/model"
log "github.com/sirupsen/logrus"
)
// GetMessages 实现 Repository 接口的 GetMessages 方法
func (r *Repository) GetMessages(ctx context.Context, startTime, endTime time.Time, talker string, limit, offset int) ([]*model.Message, error) {
if contact, _ := r.GetContact(ctx, talker); contact != nil {
talker = contact.UserName
} else if chatRoom, _ := r.GetChatRoom(ctx, talker); chatRoom != nil {
talker = chatRoom.Name
}
messages, err := r.ds.GetMessages(ctx, startTime, endTime, talker, limit, offset)
if err != nil {
return nil, err
}
// 补充消息信息
if err := r.EnrichMessages(ctx, messages); err != nil {
log.Debugf("EnrichMessages failed: %v", err)
}
return messages, nil
}
// EnrichMessages 补充消息的额外信息
func (r *Repository) EnrichMessages(ctx context.Context, messages []*model.Message) error {
for _, msg := range messages {
r.enrichMessage(msg)
}
return nil
}
// 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()
// 补充发送者在群里的显示名称
if displayName, ok := chatRoom.User2DisplayName[talker]; ok {
msg.DisplayName = displayName
}
}
}
// 如果不是自己发送的消息且还没有显示名称,尝试补充发送者信息
if msg.DisplayName == "" && msg.IsSender != 1 {
contact := r.getFullContact(talker)
if contact != nil {
msg.DisplayName = contact.DisplayName()
}
}
}

View File

@@ -0,0 +1,85 @@
package repository
import (
"context"
"fmt"
"github.com/sjzar/chatlog/internal/model"
"github.com/sjzar/chatlog/internal/wechatdb/datasource"
)
// Repository 实现了 repository.Repository 接口
type Repository struct {
ds datasource.DataSource
// Cache for contact
contactCache map[string]*model.Contact
aliasToContact map[string]*model.Contact
remarkToContact map[string]*model.Contact
nickNameToContact map[string]*model.Contact
chatRoomInContact map[string]*model.Contact
contactList []string
aliasList []string
remarkList []string
nickNameList []string
// Cache for chat room
chatRoomCache map[string]*model.ChatRoom
remarkToChatRoom map[string]*model.ChatRoom
nickNameToChatRoom map[string]*model.ChatRoom
chatRoomList []string
chatRoomRemark []string
chatRoomNickName []string
// 快速查找索引
chatRoomUserToInfo map[string]*model.Contact
}
// New 创建一个新的 Repository
func New(ds datasource.DataSource) (*Repository, error) {
r := &Repository{
ds: ds,
contactCache: make(map[string]*model.Contact),
aliasToContact: make(map[string]*model.Contact),
remarkToContact: make(map[string]*model.Contact),
nickNameToContact: make(map[string]*model.Contact),
chatRoomUserToInfo: make(map[string]*model.Contact),
contactList: make([]string, 0),
aliasList: make([]string, 0),
remarkList: make([]string, 0),
nickNameList: make([]string, 0),
chatRoomCache: make(map[string]*model.ChatRoom),
remarkToChatRoom: make(map[string]*model.ChatRoom),
nickNameToChatRoom: make(map[string]*model.ChatRoom),
chatRoomList: make([]string, 0),
chatRoomRemark: make([]string, 0),
chatRoomNickName: make([]string, 0),
}
// 初始化缓存
if err := r.initCache(context.Background()); err != nil {
return nil, fmt.Errorf("初始化缓存失败: %w", err)
}
return r, nil
}
// initCache 初始化缓存
func (r *Repository) initCache(ctx context.Context) error {
// 初始化联系人缓存
if err := r.initContactCache(ctx); err != nil {
return err
}
// 初始化群聊缓存
if err := r.initChatRoomCache(ctx); err != nil {
return err
}
return nil
}
// Close 实现 Repository 接口的 Close 方法
func (r *Repository) Close() error {
return r.ds.Close()
}

View File

@@ -0,0 +1,11 @@
package repository
import (
"context"
"github.com/sjzar/chatlog/internal/model"
)
func (r *Repository) GetSessions(ctx context.Context, key string, limit, offset int) ([]*model.Session, error) {
return r.ds.GetSessions(ctx, key, limit, offset)
}

View File

@@ -1,25 +1,31 @@
package wechatdb
import (
"context"
"fmt"
"time"
"github.com/sjzar/chatlog/pkg/model"
"github.com/sjzar/chatlog/internal/model"
"github.com/sjzar/chatlog/internal/wechatdb/datasource"
"github.com/sjzar/chatlog/internal/wechatdb/repository"
_ "github.com/mattn/go-sqlite3"
)
type DB struct {
BasePath string
Version int
contact *Contact
message *Message
path string
platform string
version int
ds datasource.DataSource
repo *repository.Repository
}
func New(path string, version int) (*DB, error) {
func New(path string, platform string, version int) (*DB, error) {
w := &DB{
BasePath: path,
Version: version,
path: path,
platform: platform,
version: version,
}
// 初始化,加载数据库文件信息
@@ -31,87 +37,87 @@ func New(path string, version int) (*DB, error) {
}
func (w *DB) Close() error {
if w.repo != nil {
return w.repo.Close()
}
return nil
}
func (w *DB) Initialize() error {
var err error
w.message, err = NewMessage(w.BasePath, w.Version)
w.ds, err = datasource.NewDataSource(w.path, w.platform, w.version)
if err != nil {
return err
return fmt.Errorf("初始化数据源失败: %w", err)
}
w.contact, err = NewContact(w.BasePath, w.Version)
w.repo, err = repository.New(w.ds)
if err != nil {
return err
return fmt.Errorf("初始化仓库失败: %w", err)
}
return nil
}
func (w *DB) GetMessages(start, end time.Time, talker string, limit, offset int) ([]*model.Message, error) {
ctx := context.Background()
if talker != "" {
if contact := w.contact.GetContact(talker); contact != nil {
talker = contact.UserName
}
}
messages, err := w.message.GetMessages(start, end, talker, limit, offset)
// 使用 repository 获取消息
messages, err := w.repo.GetMessages(ctx, start, end, talker, limit, offset)
if err != nil {
return nil, err
}
for i := range messages {
w.contact.MessageFillInfo(messages[i])
return nil, fmt.Errorf("获取消息失败: %w", err)
}
return messages, nil
}
type ListContactResp struct {
type GetContactsResp struct {
Items []*model.Contact `json:"items"`
}
func (w *DB) ListContact() (*ListContactResp, error) {
list, err := w.contact.ListContact()
func (w *DB) GetContacts(key string, limit, offset int) (*GetContactsResp, error) {
ctx := context.Background()
contacts, err := w.repo.GetContacts(ctx, key, limit, offset)
if err != nil {
return nil, err
}
return &ListContactResp{
Items: list,
return &GetContactsResp{
Items: contacts,
}, nil
}
func (w *DB) GetContact(userName string) *model.Contact {
return w.contact.GetContact(userName)
}
type ListChatRoomResp struct {
type GetChatRoomsResp struct {
Items []*model.ChatRoom `json:"items"`
}
func (w *DB) ListChatRoom() (*ListChatRoomResp, error) {
list, err := w.contact.ListChatRoom()
func (w *DB) GetChatRooms(key string, limit, offset int) (*GetChatRoomsResp, error) {
ctx := context.Background()
chatRooms, err := w.repo.GetChatRooms(ctx, key, limit, offset)
if err != nil {
return nil, err
}
return &ListChatRoomResp{
Items: list,
return &GetChatRoomsResp{
Items: chatRooms,
}, nil
}
func (w *DB) GetChatRoom(userName string) *model.ChatRoom {
return w.contact.GetChatRoom(userName)
}
type GetSessionResp struct {
type GetSessionsResp struct {
Items []*model.Session `json:"items"`
}
func (w *DB) GetSession(limit int) (*GetSessionResp, error) {
sessions := w.contact.GetSession(limit)
return &GetSessionResp{
func (w *DB) GetSessions(key string, limit, offset int) (*GetSessionsResp, error) {
ctx := context.Background()
// 使用 repository 获取会话列表
sessions, err := w.repo.GetSessions(ctx, key, limit, offset)
if err != nil {
return nil, fmt.Errorf("获取会话列表失败: %w", err)
}
return &GetSessionsResp{
Items: sessions,
}, nil
}