package wechat import ( "context" "fmt" "os" "path/filepath" "sync" "time" "github.com/fsnotify/fsnotify" "github.com/rs/zerolog/log" "github.com/sjzar/chatlog/internal/errors" "github.com/sjzar/chatlog/internal/wechat" "github.com/sjzar/chatlog/internal/wechat/decrypt" "github.com/sjzar/chatlog/pkg/filemonitor" "github.com/sjzar/chatlog/pkg/util" ) var ( DebounceTime = 1 * time.Second MaxWaitTime = 10 * time.Second ) type Service struct { conf Config lastEvents map[string]time.Time pendingActions map[string]bool mutex sync.Mutex fm *filemonitor.FileMonitor } type Config interface { GetDataKey() string GetDataDir() string GetWorkDir() string GetPlatform() string GetVersion() int } func NewService(conf Config) *Service { return &Service{ conf: conf, lastEvents: make(map[string]time.Time), pendingActions: make(map[string]bool), } } // GetWeChatInstances returns all running WeChat instances func (s *Service) GetWeChatInstances() []*wechat.Account { wechat.Load() return wechat.GetAccounts() } // GetDataKey extracts the encryption key from a WeChat process func (s *Service) GetDataKey(info *wechat.Account) (string, error) { if info == nil { return "", fmt.Errorf("no WeChat instance selected") } key, _, err := info.GetKey(context.Background()) if err != nil { return "", err } return key, nil } func (s *Service) StartAutoDecrypt() error { log.Info().Msgf("start auto decrypt, data dir: %s", s.conf.GetDataDir()) dbGroup, err := filemonitor.NewFileGroup("wechat", s.conf.GetDataDir(), `.*\.db$`, []string{"fts"}) if err != nil { return err } dbGroup.AddCallback(s.DecryptFileCallback) s.fm = filemonitor.NewFileMonitor() s.fm.AddGroup(dbGroup) if err := s.fm.Start(); err != nil { log.Debug().Err(err).Msg("failed to start file monitor") return err } return nil } func (s *Service) StopAutoDecrypt() error { if s.fm != nil { if err := s.fm.Stop(); err != nil { return err } } s.fm = nil return nil } func (s *Service) DecryptFileCallback(event fsnotify.Event) error { // Local file system // WRITE "/db_storage/message/message_0.db" // WRITE "/db_storage/message/message_0.db" // WRITE|CHMOD "/db_storage/message/message_0.db" // Syncthing // REMOVE "/app/data/db_storage/session/session.db" // CREATE "/app/data/db_storage/session/session.db" ← "/app/data/db_storage/session/.syncthing.session.db.tmp" // CHMOD "/app/data/db_storage/session/session.db" if !(event.Op.Has(fsnotify.Write) || event.Op.Has(fsnotify.Create)) { return nil } s.mutex.Lock() s.lastEvents[event.Name] = time.Now() if !s.pendingActions[event.Name] { s.pendingActions[event.Name] = true s.mutex.Unlock() go s.waitAndProcess(event.Name) } else { s.mutex.Unlock() } return nil } func (s *Service) waitAndProcess(dbFile string) { start := time.Now() for { time.Sleep(DebounceTime) s.mutex.Lock() lastEventTime := s.lastEvents[dbFile] elapsed := time.Since(lastEventTime) totalElapsed := time.Since(start) if elapsed >= DebounceTime || totalElapsed >= MaxWaitTime { s.pendingActions[dbFile] = false s.mutex.Unlock() log.Debug().Msgf("Processing file: %s", dbFile) s.DecryptDBFile(dbFile) return } s.mutex.Unlock() } } func (s *Service) DecryptDBFile(dbFile string) error { decryptor, err := decrypt.NewDecryptor(s.conf.GetPlatform(), s.conf.GetVersion()) if err != nil { return err } output := filepath.Join(s.conf.GetWorkDir(), dbFile[len(s.conf.GetDataDir()):]) if err := util.PrepareDir(filepath.Dir(output)); err != nil { return err } outputTemp := output + ".tmp" outputFile, err := os.Create(outputTemp) if err != nil { return fmt.Errorf("failed to create output file: %v", err) } defer func() { outputFile.Close() if err := os.Rename(outputTemp, output); err != nil { log.Debug().Err(err).Msgf("failed to rename %s to %s", outputTemp, output) } }() if err := decryptor.Decrypt(context.Background(), dbFile, s.conf.GetDataKey(), outputFile); err != nil { if err == errors.ErrAlreadyDecrypted { if data, err := os.ReadFile(dbFile); err == nil { outputFile.Write(data) } return nil } log.Err(err).Msgf("failed to decrypt %s", dbFile) return err } log.Debug().Msgf("Decrypted %s to %s", dbFile, output) return nil } func (s *Service) DecryptDBFiles() error { dbGroup, err := filemonitor.NewFileGroup("wechat", s.conf.GetDataDir(), `.*\.db$`, []string{"fts"}) if err != nil { return err } dbFiles, err := dbGroup.List() if err != nil { return err } for _, dbFile := range dbFiles { if err := s.DecryptDBFile(dbFile); err != nil { log.Debug().Msgf("DecryptDBFile %s failed: %v", dbFile, err) continue } } return nil }