Compare commits
6 Commits
fix/http
...
feature/ms
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d92e974ce6 | ||
|
|
4250057ba8 | ||
|
|
c12ee8bfce | ||
|
|
167a9ca873 | ||
|
|
f31953c42b | ||
|
|
98f41454fb |
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/sjzar/chatlog/internal/chatlog"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -33,11 +33,11 @@ var decryptCmd = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
m, err := chatlog.New("")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
log.Err(err).Msg("failed to create chatlog instance")
|
||||
return
|
||||
}
|
||||
if err := m.CommandDecrypt(dataDir, workDir, key, decryptPlatform, decryptVer); err != nil {
|
||||
log.Error(err)
|
||||
log.Err(err).Msg("failed to decrypt")
|
||||
return
|
||||
}
|
||||
fmt.Println("decrypt success")
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/sjzar/chatlog/internal/chatlog"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -21,12 +21,12 @@ var keyCmd = &cobra.Command{
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
m, err := chatlog.New("")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
log.Err(err).Msg("failed to create chatlog instance")
|
||||
return
|
||||
}
|
||||
ret, err := m.CommandKey(pid)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
log.Err(err).Msg("failed to get key")
|
||||
return
|
||||
}
|
||||
fmt.Println(ret)
|
||||
|
||||
@@ -1,33 +1,26 @@
|
||||
package chatlog
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/sjzar/chatlog/pkg/util"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
var Debug bool
|
||||
|
||||
func initLog(cmd *cobra.Command, args []string) {
|
||||
log.SetFormatter(&log.TextFormatter{
|
||||
FullTimestamp: true,
|
||||
CallerPrettyfier: func(f *runtime.Frame) (string, string) {
|
||||
_, filename := path.Split(f.File)
|
||||
return "", fmt.Sprintf("%s:%d", filename, f.Line)
|
||||
},
|
||||
})
|
||||
zerolog.SetGlobalLevel(zerolog.InfoLevel)
|
||||
|
||||
if Debug {
|
||||
log.SetLevel(log.DebugLevel)
|
||||
log.SetReportCaller(true)
|
||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,8 +36,8 @@ func initTuiLog(cmd *cobra.Command, args []string) {
|
||||
panic(err)
|
||||
}
|
||||
logOutput = logFD
|
||||
log.SetReportCaller(true)
|
||||
}
|
||||
|
||||
log.SetOutput(logOutput)
|
||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: logOutput, NoColor: true, TimeFormat: time.RFC3339})
|
||||
logrus.SetOutput(logOutput)
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ package chatlog
|
||||
import (
|
||||
"github.com/sjzar/chatlog/internal/chatlog"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
@@ -17,7 +17,7 @@ func init() {
|
||||
|
||||
func Execute() {
|
||||
if err := rootCmd.Execute(); err != nil {
|
||||
log.Error(err)
|
||||
log.Err(err).Msg("command execution failed")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,11 +38,11 @@ func Root(cmd *cobra.Command, args []string) {
|
||||
|
||||
m, err := chatlog.New("")
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
log.Err(err).Msg("failed to create chatlog instance")
|
||||
return
|
||||
}
|
||||
|
||||
if err := m.Run(); err != nil {
|
||||
log.Error(err)
|
||||
log.Err(err).Msg("failed to run chatlog instance")
|
||||
}
|
||||
}
|
||||
|
||||
13
go.mod
13
go.mod
@@ -8,14 +8,16 @@ require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/klauspost/compress v1.18.0
|
||||
github.com/mattn/go-sqlite3 v1.14.24
|
||||
github.com/rivo/tview v0.0.0-20250322200051-73a5bd7d6839
|
||||
github.com/pierrec/lz4/v4 v4.1.22
|
||||
github.com/rivo/tview v0.0.0-20250325173046-7b72abf45814
|
||||
github.com/rs/zerolog v1.34.0
|
||||
github.com/shirou/gopsutil/v4 v4.25.2
|
||||
github.com/sirupsen/logrus v1.9.3
|
||||
github.com/spf13/cobra v1.9.1
|
||||
github.com/spf13/viper v1.20.0
|
||||
github.com/spf13/viper v1.20.1
|
||||
golang.org/x/crypto v0.36.0
|
||||
golang.org/x/sys v0.31.0
|
||||
google.golang.org/protobuf v1.36.5
|
||||
google.golang.org/protobuf v1.36.6
|
||||
howett.net/plist v1.0.1
|
||||
)
|
||||
|
||||
@@ -40,6 +42,7 @@ require (
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
@@ -47,7 +50,7 @@ require (
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/sagikazarmark/locafero v0.8.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.14.0 // indirect
|
||||
github.com/spf13/cast v1.7.1 // indirect
|
||||
@@ -60,7 +63,7 @@ require (
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
golang.org/x/arch v0.15.0 // indirect
|
||||
golang.org/x/net v0.37.0 // indirect
|
||||
golang.org/x/net v0.38.0 // indirect
|
||||
golang.org/x/term v0.30.0 // indirect
|
||||
golang.org/x/text v0.23.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
|
||||
33
go.sum
33
go.sum
@@ -6,6 +6,7 @@ github.com/bytedance/sonic/loader v0.2.4/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFos
|
||||
github.com/cloudwego/base64x v0.1.5 h1:XPciSp1xaq2VCSt6lF0phncD4koWyULpl5bUxbfCyP4=
|
||||
github.com/cloudwego/base64x v0.1.5/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
@@ -41,6 +42,7 @@ github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIx
|
||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
@@ -68,6 +70,10 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69
|
||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||
@@ -81,21 +87,27 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||
github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
|
||||
github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
||||
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/rivo/tview v0.0.0-20250322200051-73a5bd7d6839 h1:/v0ptNHBQaQCxlvS4QLxLKKGfsSA9hcZcNgqVgmPRro=
|
||||
github.com/rivo/tview v0.0.0-20250322200051-73a5bd7d6839/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss=
|
||||
github.com/rivo/tview v0.0.0-20250325173046-7b72abf45814 h1:pJIO3sp+rkDbJTeqqpe2Oihq3hegiM5ASvsd6S0pvjg=
|
||||
github.com/rivo/tview v0.0.0-20250325173046-7b72abf45814/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.8.0 h1:mXaMVw7IqxNBxfv3LdWt9MDmcWDQ1fagDH918lOdVaQ=
|
||||
github.com/sagikazarmark/locafero v0.8.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
|
||||
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
|
||||
github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
|
||||
github.com/shirou/gopsutil/v4 v4.25.2 h1:NMscG3l2CqtWFS86kj3vP7soOczqrQYIEhO/pMvvQkk=
|
||||
github.com/shirou/gopsutil/v4 v4.25.2/go.mod h1:34gBYJzyqCDT11b6bMHP0XCvWeU3J61XRT7a2EmCRTA=
|
||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||
@@ -110,8 +122,8 @@ github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=
|
||||
github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0=
|
||||
github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o=
|
||||
github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.20.0 h1:zrxIyR3RQIOsarIrgL8+sAvALXul9jeEPa06Y0Ph6vY=
|
||||
github.com/spf13/viper v1.20.0/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||
github.com/spf13/viper v1.20.1 h1:ZMi+z/lvLyPSCoNtFCpqjy0S4kPbirhpTMwl8BkW9X4=
|
||||
github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqjJvu4=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -161,8 +173,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
|
||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||
golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c=
|
||||
golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
@@ -178,6 +190,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
@@ -217,8 +230,8 @@ golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
|
||||
golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58=
|
||||
golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
|
||||
google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
|
||||
gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
|
||||
@@ -338,8 +338,8 @@ func (a *App) settingSelected(i *menu.Item) {
|
||||
|
||||
settings := []settingItem{
|
||||
{
|
||||
name: "设置 HTTP 服务端口",
|
||||
description: "配置 HTTP 服务监听的端口",
|
||||
name: "设置 HTTP 服务地址",
|
||||
description: "配置 HTTP 服务监听的地址",
|
||||
action: a.settingHTTPPort,
|
||||
},
|
||||
{
|
||||
@@ -373,17 +373,17 @@ func (a *App) settingHTTPPort() {
|
||||
// 实现端口设置逻辑
|
||||
// 这里可以使用 tview.InputField 让用户输入端口
|
||||
form := tview.NewForm().
|
||||
AddInputField("端口", a.ctx.HTTPAddr, 20, nil, func(text string) {
|
||||
a.ctx.SetHTTPAddr(text)
|
||||
AddInputField("地址", a.ctx.HTTPAddr, 20, nil, func(text string) {
|
||||
a.m.SetHTTPAddr(text)
|
||||
}).
|
||||
AddButton("保存", func() {
|
||||
a.mainPages.RemovePage("submenu2")
|
||||
a.showInfo("HTTP 端口已设置为 " + a.ctx.HTTPAddr)
|
||||
a.showInfo("HTTP 地址已设置为 " + a.ctx.HTTPAddr)
|
||||
}).
|
||||
AddButton("取消", func() {
|
||||
a.mainPages.RemovePage("submenu2")
|
||||
})
|
||||
form.SetBorder(true).SetTitle("设置 HTTP 端口")
|
||||
form.SetBorder(true).SetTitle("设置 HTTP 地址")
|
||||
|
||||
a.mainPages.AddPage("submenu2", form, true, true)
|
||||
a.SetFocus(form)
|
||||
|
||||
@@ -57,6 +57,10 @@ func (s *Service) GetSessions(key string, limit, offset int) (*wechatdb.GetSessi
|
||||
return s.db.GetSessions(key, limit, offset)
|
||||
}
|
||||
|
||||
func (s *Service) GetMedia(_type string, key string) (*model.Media, error) {
|
||||
return s.db.GetMedia(_type, key)
|
||||
}
|
||||
|
||||
// Close closes the database connection
|
||||
func (s *Service) Close() {
|
||||
// Add cleanup code if needed
|
||||
|
||||
@@ -5,10 +5,13 @@ import (
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/errors"
|
||||
"github.com/sjzar/chatlog/pkg/util"
|
||||
"github.com/sjzar/chatlog/pkg/util/dat2img"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -27,6 +30,12 @@ func (s *Service) initRouter() {
|
||||
router.StaticFileFS("/favicon.ico", "./favicon.ico", http.FS(staticDir))
|
||||
router.StaticFileFS("/", "./index.htm", http.FS(staticDir))
|
||||
|
||||
// Media
|
||||
router.GET("/image/:key", s.GetImage)
|
||||
router.GET("/video/:key", s.GetVideo)
|
||||
router.GET("/file/:key", s.GetFile)
|
||||
router.GET("/data/*path", s.GetMediaData)
|
||||
|
||||
// MCP Server
|
||||
{
|
||||
router.GET("/sse", s.mcp.HandleSSE)
|
||||
@@ -79,7 +88,7 @@ func (s *Service) GetChatlog(c *gin.Context) {
|
||||
var err error
|
||||
start, end, ok := util.TimeRangeOf(q.Time)
|
||||
if !ok {
|
||||
errors.Err(c, errors.ErrInvalidArg("time"))
|
||||
errors.Err(c, errors.InvalidArg("time"))
|
||||
}
|
||||
if q.Limit < 0 {
|
||||
q.Limit = 0
|
||||
@@ -108,7 +117,7 @@ func (s *Service) GetChatlog(c *gin.Context) {
|
||||
c.Writer.Flush()
|
||||
|
||||
for _, m := range messages {
|
||||
c.Writer.WriteString(m.PlainText(len(q.Talker) == 0))
|
||||
c.Writer.WriteString(m.PlainText(len(q.Talker) == 0, c.Request.Host))
|
||||
c.Writer.WriteString("\n")
|
||||
c.Writer.Flush()
|
||||
}
|
||||
@@ -251,3 +260,86 @@ func (s *Service) GetSessions(c *gin.Context) {
|
||||
c.Writer.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) GetImage(c *gin.Context) {
|
||||
s.GetMedia(c, "image")
|
||||
}
|
||||
|
||||
func (s *Service) GetVideo(c *gin.Context) {
|
||||
s.GetMedia(c, "video")
|
||||
}
|
||||
|
||||
func (s *Service) GetFile(c *gin.Context) {
|
||||
s.GetMedia(c, "file")
|
||||
}
|
||||
|
||||
func (s *Service) GetMedia(c *gin.Context, _type string) {
|
||||
key := c.Param("key")
|
||||
if key == "" {
|
||||
errors.Err(c, errors.InvalidArg(key))
|
||||
return
|
||||
}
|
||||
|
||||
media, err := s.db.GetMedia(_type, key)
|
||||
if err != nil {
|
||||
errors.Err(c, err)
|
||||
return
|
||||
}
|
||||
|
||||
if c.Query("info") != "" {
|
||||
c.JSON(http.StatusOK, media)
|
||||
return
|
||||
}
|
||||
|
||||
c.Redirect(http.StatusFound, "/data/"+media.Path)
|
||||
}
|
||||
|
||||
func (s *Service) GetMediaData(c *gin.Context) {
|
||||
relativePath := filepath.Clean(c.Param("path"))
|
||||
|
||||
absolutePath := filepath.Join(s.ctx.DataDir, relativePath)
|
||||
|
||||
if _, err := os.Stat(absolutePath); os.IsNotExist(err) {
|
||||
c.JSON(http.StatusNotFound, gin.H{
|
||||
"error": "File not found",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ext := strings.ToLower(filepath.Ext(absolutePath))
|
||||
switch {
|
||||
case ext == ".dat":
|
||||
s.HandleDatFile(c, absolutePath)
|
||||
default:
|
||||
// 直接返回文件
|
||||
c.File(absolutePath)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *Service) HandleDatFile(c *gin.Context, path string) {
|
||||
|
||||
b, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
errors.Err(c, err)
|
||||
return
|
||||
}
|
||||
out, ext, err := dat2img.Dat2Image(b)
|
||||
if err != nil {
|
||||
c.File(path)
|
||||
return
|
||||
}
|
||||
|
||||
switch ext {
|
||||
case "jpg":
|
||||
c.Data(http.StatusOK, "image/jpeg", out)
|
||||
case "png":
|
||||
c.Data(http.StatusOK, "image/png", out)
|
||||
case "gif":
|
||||
c.Data(http.StatusOK, "image/gif", out)
|
||||
case "bmp":
|
||||
c.Data(http.StatusOK, "image/bmp", out)
|
||||
default:
|
||||
c.File(path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ import (
|
||||
"github.com/sjzar/chatlog/internal/errors"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -33,14 +33,14 @@ func NewService(ctx *ctx.Context, db *database.Service, mcp *mcp.Service) *Servi
|
||||
|
||||
// Handle error from SetTrustedProxies
|
||||
if err := router.SetTrustedProxies(nil); err != nil {
|
||||
log.Error("Failed to set trusted proxies:", err)
|
||||
log.Err(err).Msg("Failed to set trusted proxies")
|
||||
}
|
||||
|
||||
// Middleware
|
||||
router.Use(
|
||||
errors.RecoveryMiddleware(),
|
||||
errors.ErrorHandlerMiddleware(),
|
||||
gin.LoggerWithWriter(log.StandardLogger().Out),
|
||||
gin.LoggerWithWriter(log.Logger),
|
||||
)
|
||||
|
||||
s := &Service{
|
||||
@@ -68,11 +68,11 @@ func (s *Service) Start() error {
|
||||
go func() {
|
||||
// Handle error from Run
|
||||
if err := s.server.ListenAndServe(); err != nil {
|
||||
log.Error("Server Stopped: ", err)
|
||||
log.Err(err).Msg("Failed to start HTTP server")
|
||||
}
|
||||
}()
|
||||
|
||||
log.Info("Server started on ", s.ctx.HTTPAddr)
|
||||
log.Info().Msg("Starting HTTP server on " + s.ctx.HTTPAddr)
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -88,10 +88,10 @@ func (s *Service) Stop() error {
|
||||
defer cancel()
|
||||
|
||||
if err := s.server.Shutdown(ctx); err != nil {
|
||||
return errors.HTTP("HTTP server shutdown error", err)
|
||||
return errors.HTTPShutDown(err)
|
||||
}
|
||||
|
||||
log.Info("HTTP server stopped")
|
||||
log.Info().Msg("HTTP server stopped")
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/chatlog/conf"
|
||||
"github.com/sjzar/chatlog/internal/chatlog/ctx"
|
||||
@@ -12,6 +13,7 @@ import (
|
||||
"github.com/sjzar/chatlog/internal/chatlog/mcp"
|
||||
"github.com/sjzar/chatlog/internal/chatlog/wechat"
|
||||
"github.com/sjzar/chatlog/pkg/util"
|
||||
"github.com/sjzar/chatlog/pkg/util/dat2img"
|
||||
)
|
||||
|
||||
// Manager 管理聊天日志应用
|
||||
@@ -95,6 +97,11 @@ func (m *Manager) StartService() error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 如果是 4.0 版本,更新下 xorkey
|
||||
if m.ctx.Version == 4 {
|
||||
go dat2img.ScanAndSetXorKey(m.ctx.DataDir)
|
||||
}
|
||||
|
||||
// 更新状态
|
||||
m.ctx.SetHTTPEnabled(true)
|
||||
|
||||
@@ -128,6 +135,21 @@ func (m *Manager) StopService() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) SetHTTPAddr(text string) error {
|
||||
var addr string
|
||||
if util.IsNumeric(text) {
|
||||
addr = fmt.Sprintf("0.0.0.0:%s", text)
|
||||
} else if strings.HasPrefix(text, "http://") {
|
||||
addr = strings.TrimPrefix(text, "http://")
|
||||
} else if strings.HasPrefix(text, "https://") {
|
||||
addr = strings.TrimPrefix(text, "https://")
|
||||
} else {
|
||||
addr = text
|
||||
}
|
||||
m.ctx.SetHTTPAddr(addr)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *Manager) GetDataKey() error {
|
||||
if m.ctx.Current == nil {
|
||||
return fmt.Errorf("未选择任何账号")
|
||||
|
||||
@@ -26,6 +26,7 @@ var (
|
||||
"description": "联系人的搜索关键词,可以是姓名、备注名或ID。",
|
||||
},
|
||||
},
|
||||
Required: []string{"query"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -40,6 +41,7 @@ var (
|
||||
"description": "群聊的搜索关键词,可以是群名称、群ID或相关描述",
|
||||
},
|
||||
},
|
||||
Required: []string{"query"},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -67,6 +69,7 @@ var (
|
||||
"description": "交谈对象,可以是联系人或群聊。支持使用ID、昵称、备注名等进行查询。",
|
||||
},
|
||||
},
|
||||
Required: []string{"time", "talker"},
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -191,16 +191,13 @@ func (s *Service) toolsCall(session *mcp.Session, req *mcp.Request) error {
|
||||
talker = v.(string)
|
||||
}
|
||||
limit := util.MustAnyToInt(callReq.Arguments["limit"])
|
||||
if limit == 0 {
|
||||
limit = 100
|
||||
}
|
||||
offset := util.MustAnyToInt(callReq.Arguments["offset"])
|
||||
messages, err := s.db.GetMessages(start, end, talker, limit, offset)
|
||||
if err != nil {
|
||||
return fmt.Errorf("无法获取聊天记录: %v", err)
|
||||
}
|
||||
for _, m := range messages {
|
||||
buf.WriteString(m.PlainText(len(talker) == 0))
|
||||
buf.WriteString(m.PlainText(len(talker) == 0, ""))
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
default:
|
||||
@@ -264,16 +261,13 @@ func (s *Service) resourcesRead(session *mcp.Session, req *mcp.Request) error {
|
||||
return fmt.Errorf("无法解析时间范围")
|
||||
}
|
||||
limit := util.MustAnyToInt(u.Query().Get("limit"))
|
||||
if limit == 0 {
|
||||
limit = 100
|
||||
}
|
||||
offset := util.MustAnyToInt(u.Query().Get("offset"))
|
||||
messages, err := s.db.GetMessages(start, end, u.Host, limit, offset)
|
||||
if err != nil {
|
||||
return fmt.Errorf("无法获取聊天记录: %v", err)
|
||||
}
|
||||
for _, m := range messages {
|
||||
buf.WriteString(m.PlainText(len(u.Host) == 0))
|
||||
buf.WriteString(m.PlainText(len(u.Host) == 0, ""))
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
default:
|
||||
|
||||
@@ -13,7 +13,7 @@ import (
|
||||
"github.com/sjzar/chatlog/internal/wechat/decrypt"
|
||||
"github.com/sjzar/chatlog/pkg/util"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
type Service struct {
|
||||
@@ -64,7 +64,7 @@ func (s *Service) FindDBFiles(rootDir string, recursive bool) ([]string, error)
|
||||
walkFunc := func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
// If a file or directory can't be accessed, log the error but continue
|
||||
fmt.Printf("Warning: Cannot access %s: %v\n", path, err)
|
||||
log.Err(err).Msgf("Warning: Cannot access %s", path)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -121,7 +121,7 @@ func (s *Service) DecryptDBFiles(dataDir string, workDir string, key string, pla
|
||||
defer outputFile.Close()
|
||||
|
||||
if err := decryptor.Decrypt(ctx, dbfile, key, outputFile); err != nil {
|
||||
log.Debugf("failed to decrypt %s: %v", dbfile, err)
|
||||
log.Err(err).Msgf("failed to decrypt %s", dbfile)
|
||||
if err == errors.ErrAlreadyDecrypted {
|
||||
if data, err := os.ReadFile(dbfile); err == nil {
|
||||
outputFile.Write(data)
|
||||
|
||||
@@ -1,151 +0,0 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
// 微信相关错误
|
||||
|
||||
// WeChatProcessNotFound 创建微信进程未找到错误
|
||||
func WeChatProcessNotFound() *AppError {
|
||||
return New(ErrTypeWeChat, "wechat process not found", nil, http.StatusNotFound).WithStack()
|
||||
}
|
||||
|
||||
// WeChatKeyExtractFailed 创建微信密钥提取失败错误
|
||||
func WeChatKeyExtractFailed(cause error) *AppError {
|
||||
return New(ErrTypeWeChat, "failed to extract wechat key", cause, http.StatusInternalServerError).WithStack()
|
||||
}
|
||||
|
||||
// WeChatDecryptFailed 创建微信解密失败错误
|
||||
func WeChatDecryptFailed(cause error) *AppError {
|
||||
return New(ErrTypeWeChat, "failed to decrypt wechat database", cause, http.StatusInternalServerError).WithStack()
|
||||
}
|
||||
|
||||
// WeChatAccountNotSelected 创建未选择微信账号错误
|
||||
func WeChatAccountNotSelected() *AppError {
|
||||
return New(ErrTypeWeChat, "no wechat account selected", nil, http.StatusBadRequest).WithStack()
|
||||
}
|
||||
|
||||
// 数据库相关错误
|
||||
|
||||
// DBConnectionFailed 创建数据库连接失败错误
|
||||
func DBConnectionFailed(cause error) *AppError {
|
||||
return New(ErrTypeDatabase, "database connection failed", cause, http.StatusInternalServerError).WithStack()
|
||||
}
|
||||
|
||||
// DBQueryFailed 创建数据库查询失败错误
|
||||
func DBQueryFailed(operation string, cause error) *AppError {
|
||||
return New(ErrTypeDatabase, fmt.Sprintf("database query failed: %s", operation), cause, http.StatusInternalServerError).WithStack()
|
||||
}
|
||||
|
||||
// DBRecordNotFound 创建数据库记录未找到错误
|
||||
func DBRecordNotFound(resource string) *AppError {
|
||||
return New(ErrTypeNotFound, fmt.Sprintf("record not found: %s", resource), nil, http.StatusNotFound).WithStack()
|
||||
}
|
||||
|
||||
// 配置相关错误
|
||||
|
||||
// ConfigInvalid 创建配置无效错误
|
||||
func ConfigInvalid(field string, cause error) *AppError {
|
||||
return New(ErrTypeConfig, fmt.Sprintf("invalid configuration: %s", field), cause, http.StatusInternalServerError).WithStack()
|
||||
}
|
||||
|
||||
// ConfigMissing 创建配置缺失错误
|
||||
func ConfigMissing(field string) *AppError {
|
||||
return New(ErrTypeConfig, fmt.Sprintf("missing configuration: %s", field), nil, http.StatusBadRequest).WithStack()
|
||||
}
|
||||
|
||||
// 平台相关错误
|
||||
|
||||
// PlatformUnsupported 创建不支持的平台错误
|
||||
func PlatformUnsupported(platform string, version int) *AppError {
|
||||
return New(ErrTypeInvalidArg, fmt.Sprintf("unsupported platform: %s v%d", platform, version), nil, http.StatusBadRequest).WithStack()
|
||||
}
|
||||
|
||||
// 文件系统错误
|
||||
|
||||
// FileNotFound 创建文件未找到错误
|
||||
func FileNotFound(path string) *AppError {
|
||||
return New(ErrTypeNotFound, fmt.Sprintf("file not found: %s", path), nil, http.StatusNotFound).WithStack()
|
||||
}
|
||||
|
||||
// FileReadFailed 创建文件读取失败错误
|
||||
func FileReadFailed(path string, cause error) *AppError {
|
||||
return New(ErrTypeInternal, fmt.Sprintf("failed to read file: %s", path), cause, http.StatusInternalServerError).WithStack()
|
||||
}
|
||||
|
||||
// FileWriteFailed 创建文件写入失败错误
|
||||
func FileWriteFailed(path string, cause error) *AppError {
|
||||
return New(ErrTypeInternal, fmt.Sprintf("failed to write file: %s", path), cause, http.StatusInternalServerError).WithStack()
|
||||
}
|
||||
|
||||
// 参数验证错误
|
||||
|
||||
// RequiredParam 创建必需参数缺失错误
|
||||
func RequiredParam(param string) *AppError {
|
||||
return New(ErrTypeInvalidArg, fmt.Sprintf("required parameter missing: %s", param), nil, http.StatusBadRequest).WithStack()
|
||||
}
|
||||
|
||||
// InvalidParam 创建参数无效错误
|
||||
func InvalidParam(param string, reason string) *AppError {
|
||||
message := fmt.Sprintf("invalid parameter: %s", param)
|
||||
if reason != "" {
|
||||
message = fmt.Sprintf("%s (%s)", message, reason)
|
||||
}
|
||||
return New(ErrTypeInvalidArg, message, nil, http.StatusBadRequest).WithStack()
|
||||
}
|
||||
|
||||
// 解密相关错误
|
||||
|
||||
// DecryptInvalidKey 创建无效密钥格式错误
|
||||
func DecryptInvalidKey(cause error) *AppError {
|
||||
return New(ErrTypeWeChat, "invalid key format", cause, http.StatusBadRequest).
|
||||
WithStack()
|
||||
}
|
||||
|
||||
// DecryptCreateCipherFailed 创建无法创建加密器错误
|
||||
func DecryptCreateCipherFailed(cause error) *AppError {
|
||||
return New(ErrTypeWeChat, "failed to create cipher", cause, http.StatusInternalServerError).
|
||||
WithStack()
|
||||
}
|
||||
|
||||
// DecryptDecodeKeyFailed 创建无法解码十六进制密钥错误
|
||||
func DecryptDecodeKeyFailed(cause error) *AppError {
|
||||
return New(ErrTypeWeChat, "failed to decode hex key", cause, http.StatusBadRequest).
|
||||
WithStack()
|
||||
}
|
||||
|
||||
// DecryptWriteOutputFailed 创建无法写入输出错误
|
||||
func DecryptWriteOutputFailed(cause error) *AppError {
|
||||
return New(ErrTypeWeChat, "failed to write decryption output", cause, http.StatusInternalServerError).
|
||||
WithStack()
|
||||
}
|
||||
|
||||
// DecryptOperationCanceled 创建解密操作被取消错误
|
||||
func DecryptOperationCanceled() *AppError {
|
||||
return New(ErrTypeWeChat, "decryption operation was canceled", nil, http.StatusBadRequest).
|
||||
WithStack()
|
||||
}
|
||||
|
||||
// DecryptOpenFileFailed 创建无法打开数据库文件错误
|
||||
func DecryptOpenFileFailed(path string, cause error) *AppError {
|
||||
return New(ErrTypeWeChat, fmt.Sprintf("failed to open database file: %s", path), cause, http.StatusInternalServerError).
|
||||
WithStack()
|
||||
}
|
||||
|
||||
// DecryptReadFileFailed 创建无法读取数据库文件错误
|
||||
func DecryptReadFileFailed(path string, cause error) *AppError {
|
||||
return New(ErrTypeWeChat, fmt.Sprintf("failed to read database file: %s", path), cause, http.StatusInternalServerError).
|
||||
WithStack()
|
||||
}
|
||||
|
||||
// DecryptIncompleteRead 创建不完整的头部读取错误
|
||||
func DecryptIncompleteRead(cause error) *AppError {
|
||||
return New(ErrTypeWeChat, "incomplete header read during decryption", cause, http.StatusInternalServerError).
|
||||
WithStack()
|
||||
}
|
||||
|
||||
var ErrAlreadyDecrypted = New(ErrTypeWeChat, "database file is already decrypted", nil, http.StatusBadRequest)
|
||||
var ErrDecryptHashVerificationFailed = New(ErrTypeWeChat, "hash verification failed during decryption", nil, http.StatusBadRequest)
|
||||
var ErrDecryptIncorrectKey = New(ErrTypeWeChat, "incorrect decryption key", nil, http.StatusBadRequest)
|
||||
@@ -10,51 +10,29 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// 定义错误类型常量
|
||||
const (
|
||||
ErrTypeDatabase = "database"
|
||||
ErrTypeWeChat = "wechat"
|
||||
ErrTypeHTTP = "http"
|
||||
ErrTypeConfig = "config"
|
||||
ErrTypeInvalidArg = "invalid_argument"
|
||||
ErrTypeAuth = "authentication"
|
||||
ErrTypePermission = "permission"
|
||||
ErrTypeNotFound = "not_found"
|
||||
ErrTypeValidation = "validation"
|
||||
ErrTypeRateLimit = "rate_limit"
|
||||
ErrTypeInternal = "internal"
|
||||
)
|
||||
|
||||
// AppError 表示应用程序错误
|
||||
type AppError struct {
|
||||
Type string `json:"type"` // 错误类型
|
||||
type Error struct {
|
||||
Message string `json:"message"` // 错误消息
|
||||
Cause error `json:"-"` // 原始错误
|
||||
Code int `json:"-"` // HTTP Code
|
||||
Stack []string `json:"-"` // 错误堆栈
|
||||
RequestID string `json:"request_id,omitempty"` // 请求ID,用于跟踪
|
||||
}
|
||||
|
||||
// Error 实现 error 接口
|
||||
func (e *AppError) Error() string {
|
||||
func (e *Error) Error() string {
|
||||
if e.Cause != nil {
|
||||
return fmt.Sprintf("%s: %s: %v", e.Type, e.Message, e.Cause)
|
||||
return fmt.Sprintf("%s: %v", e.Message, e.Cause)
|
||||
}
|
||||
return fmt.Sprintf("%s: %s", e.Type, e.Message)
|
||||
return fmt.Sprintf("%s", e.Message)
|
||||
}
|
||||
|
||||
// String 返回错误的字符串表示
|
||||
func (e *AppError) String() string {
|
||||
func (e *Error) String() string {
|
||||
return e.Error()
|
||||
}
|
||||
|
||||
// Unwrap 实现 errors.Unwrap 接口,用于错误链
|
||||
func (e *AppError) Unwrap() error {
|
||||
func (e *Error) Unwrap() error {
|
||||
return e.Cause
|
||||
}
|
||||
|
||||
// WithStack 添加堆栈信息到错误
|
||||
func (e *AppError) WithStack() *AppError {
|
||||
func (e *Error) WithStack() *Error {
|
||||
const depth = 32
|
||||
var pcs [depth]uintptr
|
||||
n := runtime.Callers(2, pcs[:])
|
||||
@@ -75,32 +53,29 @@ func (e *AppError) WithStack() *AppError {
|
||||
return e
|
||||
}
|
||||
|
||||
// WithRequestID 添加请求ID到错误
|
||||
func (e *AppError) WithRequestID(requestID string) *AppError {
|
||||
e.RequestID = requestID
|
||||
return e
|
||||
}
|
||||
|
||||
// New 创建新的应用错误
|
||||
func New(errType, message string, cause error, code int) *AppError {
|
||||
return &AppError{
|
||||
Type: errType,
|
||||
func New(cause error, code int, message string) *Error {
|
||||
return &Error{
|
||||
Message: message,
|
||||
Cause: cause,
|
||||
Code: code,
|
||||
}
|
||||
}
|
||||
|
||||
// Wrap 包装现有错误为 AppError
|
||||
func Wrap(err error, errType, message string, code int) *AppError {
|
||||
func Newf(cause error, code int, format string, args ...interface{}) *Error {
|
||||
return &Error{
|
||||
Message: fmt.Sprintf(format, args...),
|
||||
Cause: cause,
|
||||
Code: http.StatusInternalServerError,
|
||||
}
|
||||
}
|
||||
|
||||
func Wrap(err error, message string, code int) *Error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 如果已经是 AppError,保留原始类型但更新消息
|
||||
if appErr, ok := err.(*AppError); ok {
|
||||
return &AppError{
|
||||
Type: appErr.Type,
|
||||
if appErr, ok := err.(*Error); ok {
|
||||
return &Error{
|
||||
Message: message,
|
||||
Cause: appErr.Cause,
|
||||
Code: appErr.Code,
|
||||
@@ -108,44 +83,15 @@ func Wrap(err error, errType, message string, code int) *AppError {
|
||||
}
|
||||
}
|
||||
|
||||
return New(errType, message, err, code)
|
||||
return New(err, code, message)
|
||||
}
|
||||
|
||||
// Is 检查错误是否为特定类型
|
||||
func Is(err error, errType string) bool {
|
||||
if err == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var appErr *AppError
|
||||
if errors.As(err, &appErr) {
|
||||
return appErr.Type == errType
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// GetType 获取错误类型
|
||||
func GetType(err error) string {
|
||||
if err == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var appErr *AppError
|
||||
if errors.As(err, &appErr) {
|
||||
return appErr.Type
|
||||
}
|
||||
|
||||
return "unknown"
|
||||
}
|
||||
|
||||
// GetCode 获取错误的 HTTP 状态码
|
||||
func GetCode(err error) int {
|
||||
if err == nil {
|
||||
return http.StatusOK
|
||||
}
|
||||
|
||||
var appErr *AppError
|
||||
var appErr *Error
|
||||
if errors.As(err, &appErr) {
|
||||
return appErr.Code
|
||||
}
|
||||
@@ -153,7 +99,6 @@ func GetCode(err error) int {
|
||||
return http.StatusInternalServerError
|
||||
}
|
||||
|
||||
// RootCause 获取错误链中的根本原因
|
||||
func RootCause(err error) error {
|
||||
for err != nil {
|
||||
unwrapped := errors.Unwrap(err)
|
||||
@@ -165,81 +110,11 @@ func RootCause(err error) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// ErrInvalidArg 无效参数错误
|
||||
func ErrInvalidArg(param string) *AppError {
|
||||
return New(ErrTypeInvalidArg, fmt.Sprintf("invalid arg: %s", param), nil, http.StatusBadRequest).WithStack()
|
||||
}
|
||||
|
||||
// Database 创建数据库错误
|
||||
func Database(message string, cause error) *AppError {
|
||||
return New(ErrTypeDatabase, message, cause, http.StatusInternalServerError).WithStack()
|
||||
}
|
||||
|
||||
// WeChat 创建微信相关错误
|
||||
func WeChat(message string, cause error) *AppError {
|
||||
return New(ErrTypeWeChat, message, cause, http.StatusInternalServerError).WithStack()
|
||||
}
|
||||
|
||||
// HTTP 创建HTTP服务错误
|
||||
func HTTP(message string, cause error) *AppError {
|
||||
return New(ErrTypeHTTP, message, cause, http.StatusInternalServerError).WithStack()
|
||||
}
|
||||
|
||||
// Config 创建配置错误
|
||||
func Config(message string, cause error) *AppError {
|
||||
return New(ErrTypeConfig, message, cause, http.StatusInternalServerError).WithStack()
|
||||
}
|
||||
|
||||
// NotFound 创建资源不存在错误
|
||||
func NotFound(resource string, cause error) *AppError {
|
||||
message := fmt.Sprintf("resource not found: %s", resource)
|
||||
return New(ErrTypeNotFound, message, cause, http.StatusNotFound).WithStack()
|
||||
}
|
||||
|
||||
// Unauthorized 创建未授权错误
|
||||
func Unauthorized(message string, cause error) *AppError {
|
||||
return New(ErrTypeAuth, message, cause, http.StatusUnauthorized).WithStack()
|
||||
}
|
||||
|
||||
// Forbidden 创建权限不足错误
|
||||
func Forbidden(message string, cause error) *AppError {
|
||||
return New(ErrTypePermission, message, cause, http.StatusForbidden).WithStack()
|
||||
}
|
||||
|
||||
// Validation 创建数据验证错误
|
||||
func Validation(message string, cause error) *AppError {
|
||||
return New(ErrTypeValidation, message, cause, http.StatusBadRequest).WithStack()
|
||||
}
|
||||
|
||||
// RateLimit 创建请求频率限制错误
|
||||
func RateLimit(message string, cause error) *AppError {
|
||||
return New(ErrTypeRateLimit, message, cause, http.StatusTooManyRequests).WithStack()
|
||||
}
|
||||
|
||||
// Internal 创建内部服务器错误
|
||||
func Internal(message string, cause error) *AppError {
|
||||
return New(ErrTypeInternal, message, cause, http.StatusInternalServerError).WithStack()
|
||||
}
|
||||
|
||||
// Err 在HTTP响应中返回错误
|
||||
func Err(c *gin.Context, err error) {
|
||||
// 获取请求ID(如果有)
|
||||
requestID := c.GetString("RequestID")
|
||||
|
||||
if appErr, ok := err.(*AppError); ok {
|
||||
if requestID != "" {
|
||||
appErr.RequestID = requestID
|
||||
}
|
||||
if appErr, ok := err.(*Error); ok {
|
||||
c.JSON(appErr.Code, appErr)
|
||||
return
|
||||
}
|
||||
|
||||
// 未知错误
|
||||
unknownErr := &AppError{
|
||||
Type: "unknown",
|
||||
Message: err.Error(),
|
||||
Code: http.StatusInternalServerError,
|
||||
RequestID: requestID,
|
||||
}
|
||||
c.JSON(http.StatusInternalServerError, unknownErr)
|
||||
c.JSON(http.StatusInternalServerError, err.Error())
|
||||
}
|
||||
|
||||
@@ -1,165 +0,0 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestErrorCreation(t *testing.T) {
|
||||
// 测试创建基本错误
|
||||
err := New("test", "test message", nil, http.StatusBadRequest)
|
||||
if err.Type != "test" || err.Message != "test message" || err.Code != http.StatusBadRequest {
|
||||
t.Errorf("New() created incorrect error: %v", err)
|
||||
}
|
||||
|
||||
// 测试创建带原因的错误
|
||||
cause := fmt.Errorf("original error")
|
||||
err = New("test", "test with cause", cause, http.StatusInternalServerError)
|
||||
if err.Cause != cause {
|
||||
t.Errorf("New() did not set cause correctly: %v", err)
|
||||
}
|
||||
|
||||
// 测试错误消息格式
|
||||
expected := "test: test with cause: original error"
|
||||
if err.Error() != expected {
|
||||
t.Errorf("Error() = %q, want %q", err.Error(), expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorWrapping(t *testing.T) {
|
||||
// 测试包装普通错误
|
||||
original := fmt.Errorf("original error")
|
||||
wrapped := Wrap(original, "wrapped", "wrapped message", http.StatusBadRequest)
|
||||
|
||||
if wrapped.Type != "wrapped" || wrapped.Message != "wrapped message" {
|
||||
t.Errorf("Wrap() created incorrect error: %v", wrapped)
|
||||
}
|
||||
|
||||
if wrapped.Cause != original {
|
||||
t.Errorf("Wrap() did not set cause correctly")
|
||||
}
|
||||
|
||||
// 测试包装 AppError
|
||||
appErr := New("app", "app error", nil, http.StatusNotFound)
|
||||
rewrapped := Wrap(appErr, "ignored", "new message", http.StatusBadRequest)
|
||||
|
||||
if rewrapped.Type != "app" {
|
||||
t.Errorf("Wrap() did not preserve original AppError type: got %s, want %s",
|
||||
rewrapped.Type, appErr.Type)
|
||||
}
|
||||
|
||||
if rewrapped.Message != "new message" {
|
||||
t.Errorf("Wrap() did not update message: got %s, want %s",
|
||||
rewrapped.Message, "new message")
|
||||
}
|
||||
|
||||
if rewrapped.Code != appErr.Code {
|
||||
t.Errorf("Wrap() did not preserve original status code: got %d, want %d",
|
||||
rewrapped.Code, appErr.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorTypeChecking(t *testing.T) {
|
||||
// 创建不同类型的错误
|
||||
dbErr := Database("db error", nil)
|
||||
httpErr := HTTP("http error", nil)
|
||||
|
||||
// 测试 Is 函数
|
||||
if !Is(dbErr, ErrTypeDatabase) {
|
||||
t.Errorf("Is() failed to identify database error")
|
||||
}
|
||||
|
||||
if Is(dbErr, ErrTypeHTTP) {
|
||||
t.Errorf("Is() incorrectly identified database error as HTTP error")
|
||||
}
|
||||
|
||||
if !Is(httpErr, ErrTypeHTTP) {
|
||||
t.Errorf("Is() failed to identify HTTP error")
|
||||
}
|
||||
|
||||
// 测试 GetType 函数
|
||||
if GetType(dbErr) != ErrTypeDatabase {
|
||||
t.Errorf("GetType() returned incorrect type: got %s, want %s",
|
||||
GetType(dbErr), ErrTypeDatabase)
|
||||
}
|
||||
|
||||
if GetType(httpErr) != ErrTypeHTTP {
|
||||
t.Errorf("GetType() returned incorrect type: got %s, want %s",
|
||||
GetType(httpErr), ErrTypeHTTP)
|
||||
}
|
||||
|
||||
// 测试普通错误
|
||||
stdErr := fmt.Errorf("standard error")
|
||||
if GetType(stdErr) != "unknown" {
|
||||
t.Errorf("GetType() for standard error should return 'unknown', got %s",
|
||||
GetType(stdErr))
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorUnwrapping(t *testing.T) {
|
||||
// 创建嵌套错误
|
||||
innermost := fmt.Errorf("innermost error")
|
||||
inner := Wrap(innermost, "inner", "inner error", http.StatusBadRequest)
|
||||
outer := Wrap(inner, "outer", "outer error", http.StatusInternalServerError)
|
||||
|
||||
// 测试 Unwrap
|
||||
if unwrapped := outer.Unwrap(); unwrapped != inner.Cause {
|
||||
t.Errorf("Unwrap() did not return correct inner error")
|
||||
}
|
||||
|
||||
// 测试 RootCause
|
||||
if root := RootCause(outer); root != innermost {
|
||||
t.Errorf("RootCause() did not return innermost error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorHelperFunctions(t *testing.T) {
|
||||
// 测试辅助函数
|
||||
invalidArg := ErrInvalidArg("username")
|
||||
if invalidArg.Type != ErrTypeInvalidArg {
|
||||
t.Errorf("ErrInvalidArg() created error with wrong type: %s", invalidArg.Type)
|
||||
}
|
||||
|
||||
dbErr := Database("query failed", nil)
|
||||
if dbErr.Type != ErrTypeDatabase {
|
||||
t.Errorf("Database() created error with wrong type: %s", dbErr.Type)
|
||||
}
|
||||
|
||||
notFound := NotFound("user", nil)
|
||||
if notFound.Type != ErrTypeNotFound || notFound.Code != http.StatusNotFound {
|
||||
t.Errorf("NotFound() created error with wrong type or code: %s, %d",
|
||||
notFound.Type, notFound.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func TestErrorUtilityFunctions(t *testing.T) {
|
||||
// 测试 JoinErrors
|
||||
err1 := fmt.Errorf("error 1")
|
||||
err2 := fmt.Errorf("error 2")
|
||||
|
||||
// 单个错误
|
||||
if joined := JoinErrors(err1); joined != err1 {
|
||||
t.Errorf("JoinErrors() with single error should return that error")
|
||||
}
|
||||
|
||||
// 多个错误
|
||||
joined := JoinErrors(err1, err2)
|
||||
if joined == nil {
|
||||
t.Errorf("JoinErrors() returned nil for multiple errors")
|
||||
}
|
||||
|
||||
// nil 错误
|
||||
if joined := JoinErrors(nil, nil); joined != nil {
|
||||
t.Errorf("JoinErrors() with all nil should return nil")
|
||||
}
|
||||
|
||||
// 测试 WrapIfErr
|
||||
if wrapped := WrapIfErr(nil, "test", "message", http.StatusOK); wrapped != nil {
|
||||
t.Errorf("WrapIfErr() with nil should return nil")
|
||||
}
|
||||
|
||||
if wrapped := WrapIfErr(err1, "test", "message", http.StatusBadRequest); wrapped == nil {
|
||||
t.Errorf("WrapIfErr() with non-nil error should return non-nil")
|
||||
}
|
||||
}
|
||||
11
internal/errors/http_errors.go
Normal file
11
internal/errors/http_errors.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package errors
|
||||
|
||||
import "net/http"
|
||||
|
||||
func InvalidArg(arg string) error {
|
||||
return Newf(nil, http.StatusBadRequest, "invalid argument: %s", arg)
|
||||
}
|
||||
|
||||
func HTTPShutDown(cause error) error {
|
||||
return Newf(cause, http.StatusInternalServerError, "http server shut down")
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// ErrorHandlerMiddleware 是一个 Gin 中间件,用于统一处理请求过程中的错误
|
||||
@@ -39,21 +39,18 @@ func RecoveryMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
// 获取请求 ID
|
||||
requestID, _ := c.Get("RequestID")
|
||||
requestIDStr, _ := requestID.(string)
|
||||
|
||||
// 创建内部服务器错误
|
||||
var err *AppError
|
||||
var err *Error
|
||||
switch v := r.(type) {
|
||||
case error:
|
||||
err = Internal("panic recovered", v).WithRequestID(requestIDStr)
|
||||
err = New(v, http.StatusInternalServerError, "panic recovered")
|
||||
default:
|
||||
err = Internal(fmt.Sprintf("panic recovered: %v", r), nil).WithRequestID(requestIDStr)
|
||||
err = Newf(nil, http.StatusInternalServerError, "panic recovered: %v", r)
|
||||
}
|
||||
|
||||
// 记录错误日志
|
||||
fmt.Printf("PANIC RECOVERED: %v\n", err)
|
||||
log.Err(err).Msg("PANIC RECOVERED")
|
||||
|
||||
// 返回 500 错误
|
||||
c.JSON(http.StatusInternalServerError, err)
|
||||
|
||||
23
internal/errors/os_errors.go
Normal file
23
internal/errors/os_errors.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package errors
|
||||
|
||||
import "net/http"
|
||||
|
||||
func OpenFileFailed(path string, cause error) *Error {
|
||||
return Newf(cause, http.StatusInternalServerError, "failed to open file: %s", path).WithStack()
|
||||
}
|
||||
|
||||
func StatFileFailed(path string, cause error) *Error {
|
||||
return Newf(cause, http.StatusInternalServerError, "failed to stat file: %s", path).WithStack()
|
||||
}
|
||||
|
||||
func ReadFileFailed(path string, cause error) *Error {
|
||||
return Newf(cause, http.StatusInternalServerError, "failed to read file: %s", path).WithStack()
|
||||
}
|
||||
|
||||
func IncompleteRead(cause error) *Error {
|
||||
return New(cause, http.StatusInternalServerError, "incomplete header read during decryption").WithStack()
|
||||
}
|
||||
|
||||
func WriteOutputFailed(cause error) *Error {
|
||||
return New(cause, http.StatusInternalServerError, "failed to write output").WithStack()
|
||||
}
|
||||
@@ -1,131 +0,0 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
stderrors "errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// WrapIfErr 如果 err 不为 nil,则包装错误并返回,否则返回 nil
|
||||
func WrapIfErr(err error, errType, message string, code int) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
return Wrap(err, errType, message, code)
|
||||
}
|
||||
|
||||
// JoinErrors 将多个错误合并为一个错误
|
||||
// 如果只有一个错误不为 nil,则返回该错误
|
||||
// 如果有多个错误不为 nil,则创建一个包含所有错误信息的新错误
|
||||
func JoinErrors(errs ...error) error {
|
||||
var nonNilErrs []error
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
nonNilErrs = append(nonNilErrs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(nonNilErrs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(nonNilErrs) == 1 {
|
||||
return nonNilErrs[0]
|
||||
}
|
||||
|
||||
// 合并多个错误
|
||||
var messages []string
|
||||
for _, err := range nonNilErrs {
|
||||
messages = append(messages, err.Error())
|
||||
}
|
||||
|
||||
return Internal(
|
||||
fmt.Sprintf("multiple errors occurred: %s", strings.Join(messages, "; ")),
|
||||
nonNilErrs[0],
|
||||
)
|
||||
}
|
||||
|
||||
// IsNil 检查错误是否为 nil
|
||||
func IsNil(err error) bool {
|
||||
return err == nil
|
||||
}
|
||||
|
||||
// IsNotNil 检查错误是否不为 nil
|
||||
func IsNotNil(err error) bool {
|
||||
return err != nil
|
||||
}
|
||||
|
||||
// IsType 检查错误是否为指定类型
|
||||
func IsType(err error, errType string) bool {
|
||||
return Is(err, errType)
|
||||
}
|
||||
|
||||
// HasCause 检查错误是否包含指定的原因
|
||||
func HasCause(err error, cause error) bool {
|
||||
if err == nil || cause == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
var appErr *AppError
|
||||
if stderrors.As(err, &appErr) {
|
||||
if appErr.Cause == cause {
|
||||
return true
|
||||
}
|
||||
return HasCause(appErr.Cause, cause)
|
||||
}
|
||||
|
||||
return err == cause
|
||||
}
|
||||
|
||||
// AsAppError 将错误转换为 AppError 类型
|
||||
func AsAppError(err error) (*AppError, bool) {
|
||||
var appErr *AppError
|
||||
if stderrors.As(err, &appErr) {
|
||||
return appErr, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
// FormatErrorChain 格式化错误链,便于调试
|
||||
func FormatErrorChain(err error) string {
|
||||
if err == nil {
|
||||
return "<nil>"
|
||||
}
|
||||
|
||||
var result strings.Builder
|
||||
result.WriteString(err.Error())
|
||||
|
||||
// 获取 AppError 类型的堆栈信息
|
||||
var appErr *AppError
|
||||
if stderrors.As(err, &appErr) && len(appErr.Stack) > 0 {
|
||||
result.WriteString("\nStack Trace:\n")
|
||||
for _, frame := range appErr.Stack {
|
||||
result.WriteString(" ")
|
||||
result.WriteString(frame)
|
||||
result.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
// 递归处理错误链
|
||||
cause := stderrors.Unwrap(err)
|
||||
if cause != nil {
|
||||
result.WriteString("\nCaused by: ")
|
||||
result.WriteString(FormatErrorChain(cause))
|
||||
}
|
||||
|
||||
return result.String()
|
||||
}
|
||||
|
||||
// GetErrorDetails 返回错误的详细信息,包括类型、消息、HTTP状态码和请求ID
|
||||
func GetErrorDetails(err error) (errType string, message string, code int, requestID string) {
|
||||
if err == nil {
|
||||
return "", "", 0, ""
|
||||
}
|
||||
|
||||
var appErr *AppError
|
||||
if stderrors.As(err, &appErr) {
|
||||
return appErr.Type, appErr.Message, appErr.Code, appErr.RequestID
|
||||
}
|
||||
|
||||
return "unknown", err.Error(), 500, ""
|
||||
}
|
||||
65
internal/errors/wechat_errors.go
Normal file
65
internal/errors/wechat_errors.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package errors
|
||||
|
||||
import "net/http"
|
||||
|
||||
var (
|
||||
ErrAlreadyDecrypted = New(nil, http.StatusBadRequest, "database file is already decrypted")
|
||||
ErrDecryptHashVerificationFailed = New(nil, http.StatusBadRequest, "hash verification failed during decryption")
|
||||
ErrDecryptIncorrectKey = New(nil, http.StatusBadRequest, "incorrect decryption key")
|
||||
ErrDecryptOperationCanceled = New(nil, http.StatusBadRequest, "decryption operation was canceled")
|
||||
ErrNoMemoryRegionsFound = New(nil, http.StatusBadRequest, "no memory regions found")
|
||||
ErrReadMemoryTimeout = New(nil, http.StatusInternalServerError, "read memory timeout")
|
||||
ErrWeChatOffline = New(nil, http.StatusBadRequest, "WeChat is offline")
|
||||
ErrSIPEnabled = New(nil, http.StatusBadRequest, "SIP is enabled")
|
||||
ErrValidatorNotSet = New(nil, http.StatusBadRequest, "validator not set")
|
||||
ErrNoValidKey = New(nil, http.StatusBadRequest, "no valid key found")
|
||||
ErrWeChatDLLNotFound = New(nil, http.StatusBadRequest, "WeChatWin.dll module not found")
|
||||
)
|
||||
|
||||
func PlatformUnsupported(platform string, version int) *Error {
|
||||
return Newf(nil, http.StatusBadRequest, "unsupported platform: %s v%d", platform, version).WithStack()
|
||||
}
|
||||
|
||||
func DecryptCreateCipherFailed(cause error) *Error {
|
||||
return New(cause, http.StatusInternalServerError, "failed to create cipher").WithStack()
|
||||
}
|
||||
|
||||
func DecodeKeyFailed(cause error) *Error {
|
||||
return New(cause, http.StatusBadRequest, "failed to decode hex key").WithStack()
|
||||
}
|
||||
|
||||
func CreatePipeFileFailed(cause error) *Error {
|
||||
return New(cause, http.StatusInternalServerError, "failed to create pipe file").WithStack()
|
||||
}
|
||||
|
||||
func OpenPipeFileFailed(cause error) *Error {
|
||||
return New(cause, http.StatusInternalServerError, "failed to open pipe file").WithStack()
|
||||
}
|
||||
|
||||
func ReadPipeFileFailed(cause error) *Error {
|
||||
return New(cause, http.StatusInternalServerError, "failed to read from pipe file").WithStack()
|
||||
}
|
||||
|
||||
func RunCmdFailed(cause error) *Error {
|
||||
return New(cause, http.StatusInternalServerError, "failed to run command").WithStack()
|
||||
}
|
||||
|
||||
func ReadMemoryFailed(cause error) *Error {
|
||||
return New(cause, http.StatusInternalServerError, "failed to read memory").WithStack()
|
||||
}
|
||||
|
||||
func OpenProcessFailed(cause error) *Error {
|
||||
return New(cause, http.StatusInternalServerError, "failed to open process").WithStack()
|
||||
}
|
||||
|
||||
func WeChatAccountNotFound(name string) *Error {
|
||||
return Newf(nil, http.StatusBadRequest, "WeChat account not found: %s", name).WithStack()
|
||||
}
|
||||
|
||||
func WeChatAccountNotOnline(name string) *Error {
|
||||
return Newf(nil, http.StatusBadRequest, "WeChat account is not online: %s", name).WithStack()
|
||||
}
|
||||
|
||||
func RefreshProcessStatusFailed(cause error) *Error {
|
||||
return New(cause, http.StatusInternalServerError, "failed to refresh process status").WithStack()
|
||||
}
|
||||
62
internal/errors/wechatdb_errors.go
Normal file
62
internal/errors/wechatdb_errors.go
Normal file
@@ -0,0 +1,62 @@
|
||||
package errors
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrTalkerEmpty = New(nil, http.StatusBadRequest, "talker empty").WithStack()
|
||||
ErrKeyEmpty = New(nil, http.StatusBadRequest, "key empty").WithStack()
|
||||
ErrMediaNotFound = New(nil, http.StatusNotFound, "media not found").WithStack()
|
||||
ErrKeyLengthMust32 = New(nil, http.StatusBadRequest, "key length must be 32 bytes").WithStack()
|
||||
)
|
||||
|
||||
// 数据库初始化相关错误
|
||||
func DBFileNotFound(path, pattern string, cause error) *Error {
|
||||
return Newf(cause, http.StatusNotFound, "db file not found %s: %s", path, pattern).WithStack()
|
||||
}
|
||||
|
||||
func DBConnectFailed(path string, cause error) *Error {
|
||||
return Newf(cause, http.StatusInternalServerError, "db connect failed: %s", path).WithStack()
|
||||
}
|
||||
|
||||
func DBInitFailed(cause error) *Error {
|
||||
return New(cause, http.StatusInternalServerError, "db init failed").WithStack()
|
||||
}
|
||||
|
||||
func TalkerNotFound(talker string) *Error {
|
||||
return Newf(nil, http.StatusNotFound, "talker not found: %s", talker).WithStack()
|
||||
}
|
||||
|
||||
func DBCloseFailed(cause error) *Error {
|
||||
return New(cause, http.StatusInternalServerError, "db close failed").WithStack()
|
||||
}
|
||||
|
||||
func QueryFailed(query string, cause error) *Error {
|
||||
return Newf(cause, http.StatusInternalServerError, "query failed: %s", query).WithStack()
|
||||
}
|
||||
|
||||
func ScanRowFailed(cause error) *Error {
|
||||
return New(cause, http.StatusInternalServerError, "scan row failed").WithStack()
|
||||
}
|
||||
|
||||
func TimeRangeNotFound(start, end time.Time) *Error {
|
||||
return Newf(nil, http.StatusNotFound, "time range not found: %s - %s", start, end).WithStack()
|
||||
}
|
||||
|
||||
func MediaTypeUnsupported(_type string) *Error {
|
||||
return Newf(nil, http.StatusBadRequest, "unsupported media type: %s", _type).WithStack()
|
||||
}
|
||||
|
||||
func ChatRoomNotFound(key string) *Error {
|
||||
return Newf(nil, http.StatusNotFound, "chat room not found: %s", key).WithStack()
|
||||
}
|
||||
|
||||
func ContactNotFound(key string) *Error {
|
||||
return Newf(nil, http.StatusNotFound, "contact not found: %s", key).WithStack()
|
||||
}
|
||||
|
||||
func InitCacheFailed(cause error) *Error {
|
||||
return New(cause, http.StatusInternalServerError, "init cache failed").WithStack()
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import (
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/google/uuid"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -85,7 +85,7 @@ func (m *MCP) HandleMessages(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
log.Printf("收到消息: %v\n", req)
|
||||
log.Debug().Msgf("session: %s, request: %s", sessionID, req)
|
||||
select {
|
||||
case m.ProcessChan <- ProcessCtx{Session: session, Request: &req}:
|
||||
default:
|
||||
|
||||
@@ -94,6 +94,7 @@ type Tool struct {
|
||||
type ToolSchema struct {
|
||||
Type string `json:"type"`
|
||||
Properties M `json:"properties"`
|
||||
Required []string `json:"required,omitempty"`
|
||||
}
|
||||
|
||||
// {
|
||||
|
||||
44
internal/model/media.go
Normal file
44
internal/model/media.go
Normal file
@@ -0,0 +1,44 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
type Media struct {
|
||||
Type string `json:"type"` // 媒体类型:image, video, voice, file
|
||||
Key string `json:"key"` // MD5
|
||||
Path string `json:"path"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
ModifyTime int64 `json:"modifyTime"`
|
||||
}
|
||||
|
||||
type MediaV3 struct {
|
||||
Type string `json:"type"`
|
||||
Key string `json:"key"`
|
||||
Dir1 string `json:"dir1"`
|
||||
Dir2 string `json:"dir2"`
|
||||
Name string `json:"name"`
|
||||
ModifyTime int64 `json:"modifyTime"`
|
||||
}
|
||||
|
||||
func (m *MediaV3) Wrap() *Media {
|
||||
|
||||
var path string
|
||||
switch m.Type {
|
||||
case "image":
|
||||
path = filepath.Join("FileStorage", "MsgAttach", m.Dir1, "Image", m.Dir2, m.Name)
|
||||
case "video":
|
||||
path = filepath.Join("FileStorage", "Video", m.Dir2, m.Name)
|
||||
case "file":
|
||||
path = filepath.Join("FileStorage", "File", m.Dir2, m.Name)
|
||||
}
|
||||
|
||||
return &Media{
|
||||
Type: m.Type,
|
||||
Key: m.Key,
|
||||
ModifyTime: m.ModifyTime,
|
||||
Path: path,
|
||||
Name: m.Name,
|
||||
}
|
||||
}
|
||||
40
internal/model/media_darwinv3.go
Normal file
40
internal/model/media_darwinv3.go
Normal file
@@ -0,0 +1,40 @@
|
||||
package model
|
||||
|
||||
import "path/filepath"
|
||||
|
||||
// CREATE TABLE HlinkMediaRecord(
|
||||
// mediaMd5 TEXT,
|
||||
// mediaSize INTEGER,
|
||||
// inodeNumber INTEGER,
|
||||
// modifyTime INTEGER ,
|
||||
// CONSTRAINT _Md5_Size UNIQUE (mediaMd5,mediaSize)
|
||||
// )
|
||||
// CREATE TABLE HlinkMediaDetail(
|
||||
// localId INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
// inodeNumber INTEGER,
|
||||
// relativePath TEXT,
|
||||
// fileName TEXT
|
||||
// )
|
||||
type MediaDarwinV3 struct {
|
||||
MediaMd5 string `json:"mediaMd5"`
|
||||
MediaSize int64 `json:"mediaSize"`
|
||||
InodeNumber int64 `json:"inodeNumber"`
|
||||
ModifyTime int64 `json:"modifyTime"`
|
||||
RelativePath string `json:"relativePath"`
|
||||
FileName string `json:"fileName"`
|
||||
}
|
||||
|
||||
func (m *MediaDarwinV3) Wrap() *Media {
|
||||
|
||||
path := filepath.Join("Message/MessageTemp", m.RelativePath, m.FileName)
|
||||
name := filepath.Base(path)
|
||||
|
||||
return &Media{
|
||||
Type: "",
|
||||
Key: m.MediaMd5,
|
||||
Size: m.MediaSize,
|
||||
ModifyTime: m.ModifyTime,
|
||||
Path: path,
|
||||
Name: name,
|
||||
}
|
||||
}
|
||||
35
internal/model/media_v4.go
Normal file
35
internal/model/media_v4.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package model
|
||||
|
||||
import "path/filepath"
|
||||
|
||||
type MediaV4 struct {
|
||||
Type string `json:"type"`
|
||||
Key string `json:"key"`
|
||||
Dir1 string `json:"dir1"`
|
||||
Dir2 string `json:"dir2"`
|
||||
Name string `json:"name"`
|
||||
Size int64 `json:"size"`
|
||||
ModifyTime int64 `json:"modifyTime"`
|
||||
}
|
||||
|
||||
func (m *MediaV4) Wrap() *Media {
|
||||
|
||||
var path string
|
||||
switch m.Type {
|
||||
case "image":
|
||||
path = filepath.Join("msg", "attach", m.Dir1, m.Dir2, "Img", m.Name)
|
||||
case "video":
|
||||
path = filepath.Join("msg", "video", m.Dir1, m.Name)
|
||||
case "file":
|
||||
path = filepath.Join("msg", "file", m.Dir1, m.Name)
|
||||
}
|
||||
|
||||
return &Media{
|
||||
Type: m.Type,
|
||||
Key: m.Key,
|
||||
Path: path,
|
||||
Name: m.Name,
|
||||
Size: m.Size,
|
||||
ModifyTime: m.ModifyTime,
|
||||
}
|
||||
}
|
||||
375
internal/model/mediamessage.go
Normal file
375
internal/model/mediamessage.go
Normal file
@@ -0,0 +1,375 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type MediaMsg struct {
|
||||
XMLName xml.Name `xml:"msg"`
|
||||
Image Image `xml:"img,omitempty"`
|
||||
Video Video `xml:"videomsg,omitempty"`
|
||||
App App `xml:"appmsg,omitempty"`
|
||||
}
|
||||
|
||||
type Image struct {
|
||||
MD5 string `xml:"md5,attr"`
|
||||
// HdLength string `xml:"hdlength,attr"`
|
||||
// Length string `xml:"length,attr"`
|
||||
// AesKey string `xml:"aeskey,attr"`
|
||||
// EncryVer string `xml:"encryver,attr"`
|
||||
// OriginSourceMd5 string `xml:"originsourcemd5,attr"`
|
||||
// FileKey string `xml:"filekey,attr"`
|
||||
// UploadContinueCount string `xml:"uploadcontinuecount,attr"`
|
||||
// ImgSourceUrl string `xml:"imgsourceurl,attr"`
|
||||
// HevcMidSize string `xml:"hevc_mid_size,attr"`
|
||||
// CdnBigImgUrl string `xml:"cdnbigimgurl,attr"`
|
||||
// CdnMidImgUrl string `xml:"cdnmidimgurl,attr"`
|
||||
// CdnThumbUrl string `xml:"cdnthumburl,attr"`
|
||||
// CdnThumbLength string `xml:"cdnthumblength,attr"`
|
||||
// CdnThumbWidth string `xml:"cdnthumbwidth,attr"`
|
||||
// CdnThumbHeight string `xml:"cdnthumbheight,attr"`
|
||||
// CdnThumbAesKey string `xml:"cdnthumbaeskey,attr"`
|
||||
}
|
||||
|
||||
type Video struct {
|
||||
RawMd5 string `xml:"rawmd5,attr"`
|
||||
// Length string `xml:"length,attr"`
|
||||
// PlayLength string `xml:"playlength,attr"`
|
||||
// Offset string `xml:"offset,attr"`
|
||||
// FromUserName string `xml:"fromusername,attr"`
|
||||
// Status string `xml:"status,attr"`
|
||||
// Compress string `xml:"compress,attr"`
|
||||
// CameraType string `xml:"cameratype,attr"`
|
||||
// Source string `xml:"source,attr"`
|
||||
// AesKey string `xml:"aeskey,attr"`
|
||||
// CdnVideoUrl string `xml:"cdnvideourl,attr"`
|
||||
// CdnThumbUrl string `xml:"cdnthumburl,attr"`
|
||||
// CdnThumbLength string `xml:"cdnthumblength,attr"`
|
||||
// CdnThumbWidth string `xml:"cdnthumbwidth,attr"`
|
||||
// CdnThumbHeight string `xml:"cdnthumbheight,attr"`
|
||||
// CdnThumbAesKey string `xml:"cdnthumbaeskey,attr"`
|
||||
// EncryVer string `xml:"encryver,attr"`
|
||||
// RawLength string `xml:"rawlength,attr"`
|
||||
// CdnRawVideoUrl string `xml:"cdnrawvideourl,attr"`
|
||||
// CdnRawVideoAesKey string `xml:"cdnrawvideoaeskey,attr"`
|
||||
}
|
||||
|
||||
type App struct {
|
||||
Type int `xml:"type"`
|
||||
Title string `xml:"title"`
|
||||
Des string `xml:"des"`
|
||||
URL string `xml:"url"` // type 5 分享
|
||||
AppAttach *AppAttach `xml:"appattach,omitempty"` // type 6 文件
|
||||
MD5 string `xml:"md5,omitempty"` // type 6 文件
|
||||
RecordItem *RecordItem `xml:"recorditem,omitempty"` // type 19 合并转发
|
||||
SourceDisplayName string `xml:"sourcedisplayname,omitempty"` // type 33 小程序
|
||||
FinderFeed *FinderFeed `xml:"finderFeed,omitempty"` // type 51 视频号
|
||||
ReferMsg *ReferMsg `xml:"refermsg,omitempty"` // type 57 引用
|
||||
PatMsg *PatMsg `xml:"patMsg,omitempty"` // type 62 拍一拍
|
||||
WCPayInfo *WCPayInfo `xml:"wcpayinfo,omitempty"` // type 2000 微信转账
|
||||
}
|
||||
|
||||
// ReferMsg 表示引用消息
|
||||
type ReferMsg struct {
|
||||
Type int64 `xml:"type"`
|
||||
SvrID string `xml:"svrid"`
|
||||
FromUsr string `xml:"fromusr"`
|
||||
ChatUsr string `xml:"chatusr"`
|
||||
DisplayName string `xml:"displayname"`
|
||||
MsgSource string `xml:"msgsource"`
|
||||
Content string `xml:"content"`
|
||||
StrID string `xml:"strid"`
|
||||
CreateTime int64 `xml:"createtime"`
|
||||
}
|
||||
|
||||
// AppAttach 表示应用附件
|
||||
type AppAttach struct {
|
||||
TotalLen string `xml:"totallen"`
|
||||
AttachID string `xml:"attachid"`
|
||||
CDNAttachURL string `xml:"cdnattachurl"`
|
||||
EmoticonMD5 string `xml:"emoticonmd5"`
|
||||
AESKey string `xml:"aeskey"`
|
||||
FileExt string `xml:"fileext"`
|
||||
IsLargeFileMsg string `xml:"islargefilemsg"`
|
||||
}
|
||||
|
||||
type RecordItem struct {
|
||||
CDATA string `xml:",cdata"`
|
||||
|
||||
// 解析后的记录信息
|
||||
RecordInfo *RecordInfo
|
||||
}
|
||||
|
||||
// RecordInfo 表示聊天记录信息
|
||||
type RecordInfo struct {
|
||||
XMLName xml.Name `xml:"recordinfo"`
|
||||
FromScene string `xml:"fromscene,omitempty"`
|
||||
FavUsername string `xml:"favusername,omitempty"`
|
||||
FavCreateTime string `xml:"favcreatetime,omitempty"`
|
||||
IsChatRoom string `xml:"isChatRoom,omitempty"`
|
||||
Title string `xml:"title,omitempty"`
|
||||
Desc string `xml:"desc,omitempty"`
|
||||
Info string `xml:"info,omitempty"`
|
||||
DataList DataList `xml:"datalist,omitempty"`
|
||||
}
|
||||
|
||||
// DataList 表示数据列表
|
||||
type DataList struct {
|
||||
Count string `xml:"count,attr,omitempty"`
|
||||
DataItems []DataItem `xml:"dataitem,omitempty"`
|
||||
}
|
||||
|
||||
// DataItem 表示数据项
|
||||
type DataItem struct {
|
||||
DataType string `xml:"datatype,attr,omitempty"`
|
||||
DataID string `xml:"dataid,attr,omitempty"`
|
||||
HTMLID string `xml:"htmlid,attr,omitempty"`
|
||||
DataFmt string `xml:"datafmt,omitempty"`
|
||||
SourceName string `xml:"sourcename,omitempty"`
|
||||
SourceTime string `xml:"sourcetime,omitempty"`
|
||||
SourceHeadURL string `xml:"sourceheadurl,omitempty"`
|
||||
DataDesc string `xml:"datadesc,omitempty"`
|
||||
|
||||
// 图片特有字段
|
||||
ThumbSourcePath string `xml:"thumbsourcepath,omitempty"`
|
||||
ThumbSize string `xml:"thumbsize,omitempty"`
|
||||
CDNDataURL string `xml:"cdndataurl,omitempty"`
|
||||
CDNDataKey string `xml:"cdndatakey,omitempty"`
|
||||
CDNThumbURL string `xml:"cdnthumburl,omitempty"`
|
||||
CDNThumbKey string `xml:"cdnthumbkey,omitempty"`
|
||||
DataSourcePath string `xml:"datasourcepath,omitempty"`
|
||||
FullMD5 string `xml:"fullmd5,omitempty"`
|
||||
ThumbFullMD5 string `xml:"thumbfullmd5,omitempty"`
|
||||
ThumbHead256MD5 string `xml:"thumbhead256md5,omitempty"`
|
||||
DataSize string `xml:"datasize,omitempty"`
|
||||
CDNEncryVer string `xml:"cdnencryver,omitempty"`
|
||||
SrcChatname string `xml:"srcChatname,omitempty"`
|
||||
SrcMsgLocalID string `xml:"srcMsgLocalid,omitempty"`
|
||||
SrcMsgCreateTime string `xml:"srcMsgCreateTime,omitempty"`
|
||||
MessageUUID string `xml:"messageuuid,omitempty"`
|
||||
FromNewMsgID string `xml:"fromnewmsgid,omitempty"`
|
||||
|
||||
// 套娃合并转发
|
||||
DataTitle string `xml:"datatitle,omitempty"`
|
||||
RecordXML *RecordXML `xml:"recordxml,omitempty"`
|
||||
}
|
||||
|
||||
type RecordXML struct {
|
||||
RecordInfo RecordInfo `xml:"recordinfo,omitempty"`
|
||||
}
|
||||
|
||||
func (r *RecordInfo) String(title, host string) string {
|
||||
buf := strings.Builder{}
|
||||
if title == "" {
|
||||
title = r.Title
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf("[合并转发|%s]\n", title))
|
||||
for _, item := range r.DataList.DataItems {
|
||||
buf.WriteString(fmt.Sprintf(" %s %s\n", item.SourceName, item.SourceTime))
|
||||
|
||||
// 套娃合并转发
|
||||
if item.DataType == "17" && item.RecordXML != nil {
|
||||
content := item.RecordXML.RecordInfo.String(item.DataTitle, host)
|
||||
if content != "" {
|
||||
for _, line := range strings.Split(content, "\n") {
|
||||
buf.WriteString(fmt.Sprintf(" %s\n", line))
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
switch item.DataFmt {
|
||||
case "pic", "jpg":
|
||||
buf.WriteString(fmt.Sprintf(" \n", host, item.FullMD5))
|
||||
default:
|
||||
for _, line := range strings.Split(item.DataDesc, "\n") {
|
||||
buf.WriteString(fmt.Sprintf(" %s\n", line))
|
||||
}
|
||||
}
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
// PatMsg 拍一拍消息结构
|
||||
type PatMsg struct {
|
||||
ChatUser string `xml:"chatUser"` // 被拍的用户
|
||||
RecordNum int `xml:"recordNum"` // 记录数量
|
||||
Records Records `xml:"records"` // 拍一拍记录
|
||||
}
|
||||
|
||||
// Records 拍一拍记录集合
|
||||
type Records struct {
|
||||
Record []PatRecord `xml:"record"` // 拍一拍记录列表
|
||||
}
|
||||
|
||||
// PatRecord 单条拍一拍记录
|
||||
type PatRecord struct {
|
||||
FromUser string `xml:"fromUser"` // 发起拍一拍的用户
|
||||
PattedUser string `xml:"pattedUser"` // 被拍的用户
|
||||
Templete string `xml:"templete"` // 模板文本
|
||||
CreateTime int64 `xml:"createTime"` // 创建时间
|
||||
SvrId string `xml:"svrId"` // 服务器ID
|
||||
ReadStatus int `xml:"readStatus"` // 已读状态
|
||||
}
|
||||
|
||||
// WCPayInfo 微信支付信息
|
||||
type WCPayInfo struct {
|
||||
PaySubType int `xml:"paysubtype"` // 支付子类型
|
||||
FeeDesc string `xml:"feedesc"` // 金额描述,如"¥200000.00"
|
||||
TranscationID string `xml:"transcationid"` // 交易ID
|
||||
TransferID string `xml:"transferid"` // 转账ID
|
||||
InvalidTime string `xml:"invalidtime"` // 失效时间
|
||||
BeginTransferTime string `xml:"begintransfertime"` // 开始转账时间
|
||||
EffectiveDate string `xml:"effectivedate"` // 生效日期
|
||||
PayMemo string `xml:"pay_memo"` // 支付备注
|
||||
ReceiverUsername string `xml:"receiver_username"` // 接收方用户名
|
||||
PayerUsername string `xml:"payer_username"` // 支付方用户名
|
||||
}
|
||||
|
||||
// FinderFeed 视频号信息
|
||||
type FinderFeed struct {
|
||||
ObjectID string `xml:"objectId"`
|
||||
FeedType string `xml:"feedType"`
|
||||
Nickname string `xml:"nickname"`
|
||||
Avatar string `xml:"avatar"`
|
||||
Desc string `xml:"desc"`
|
||||
MediaCount string `xml:"mediaCount"`
|
||||
ObjectNonceID string `xml:"objectNonceId"`
|
||||
LiveID string `xml:"liveId"`
|
||||
Username string `xml:"username"`
|
||||
AuthIconURL string `xml:"authIconUrl"`
|
||||
AuthIconType int `xml:"authIconType"`
|
||||
ContactJumpInfoStr string `xml:"contactJumpInfoStr"`
|
||||
SourceCommentScene int `xml:"sourceCommentScene"`
|
||||
MediaList FinderMediaList `xml:"mediaList"`
|
||||
MegaVideo FinderMegaVideo `xml:"megaVideo"`
|
||||
BizUsername string `xml:"bizUsername"`
|
||||
BizNickname string `xml:"bizNickname"`
|
||||
BizAvatar string `xml:"bizAvatar"`
|
||||
BizUsernameV2 string `xml:"bizUsernameV2"`
|
||||
BizAuthIconURL string `xml:"bizAuthIconUrl"`
|
||||
BizAuthIconType int `xml:"bizAuthIconType"`
|
||||
EcSource string `xml:"ecSource"`
|
||||
LastGMsgID string `xml:"lastGMsgID"`
|
||||
ShareBypData string `xml:"shareBypData"`
|
||||
IsDebug int `xml:"isDebug"`
|
||||
ContentType int `xml:"content_type"`
|
||||
FinderForwardSource string `xml:"finderForwardSource"`
|
||||
}
|
||||
|
||||
type FinderMediaList struct {
|
||||
Media []FinderMedia `xml:"media"`
|
||||
}
|
||||
|
||||
type FinderMedia struct {
|
||||
ThumbURL string `xml:"thumbUrl"`
|
||||
FullCoverURL string `xml:"fullCoverUrl"`
|
||||
VideoPlayDuration string `xml:"videoPlayDuration"`
|
||||
URL string `xml:"url"`
|
||||
CoverURL string `xml:"coverUrl"`
|
||||
Height string `xml:"height"`
|
||||
MediaType string `xml:"mediaType"`
|
||||
FullClipInset string `xml:"fullClipInset"`
|
||||
Width string `xml:"width"`
|
||||
}
|
||||
|
||||
type FinderMegaVideo struct {
|
||||
ObjectID string `xml:"objectId"`
|
||||
ObjectNonceID string `xml:"objectNonceId"`
|
||||
}
|
||||
|
||||
type SysMsg struct {
|
||||
SysMsgTemplate SysMsgTemplate `xml:"sysmsgtemplate"`
|
||||
}
|
||||
|
||||
type SysMsgTemplate struct {
|
||||
ContentTemplate ContentTemplate `xml:"content_template"`
|
||||
}
|
||||
|
||||
type ContentTemplate struct {
|
||||
Type string `xml:"type,attr"`
|
||||
Plain string `xml:"plain"`
|
||||
Template string `xml:"template"`
|
||||
LinkList LinkList `xml:"link_list"`
|
||||
}
|
||||
|
||||
type LinkList struct {
|
||||
Links []Link `xml:"link"`
|
||||
}
|
||||
|
||||
type Link struct {
|
||||
Name string `xml:"name,attr"`
|
||||
Type string `xml:"type,attr"`
|
||||
MemberList MemberList `xml:"memberlist"`
|
||||
Separator string `xml:"separator"`
|
||||
}
|
||||
|
||||
type MemberList struct {
|
||||
Members []Member `xml:"member"`
|
||||
}
|
||||
|
||||
type Member struct {
|
||||
Username string `xml:"username"`
|
||||
Nickname string `xml:"nickname"`
|
||||
}
|
||||
|
||||
func (s *SysMsg) String() string {
|
||||
template := s.SysMsgTemplate.ContentTemplate.Template
|
||||
links := s.SysMsgTemplate.ContentTemplate.LinkList.Links
|
||||
|
||||
// 创建一个映射,用于存储占位符名称和对应的替换内容
|
||||
replacements := make(map[string]string)
|
||||
|
||||
// 遍历所有链接,为每个占位符准备替换内容
|
||||
for _, link := range links {
|
||||
var replacement string
|
||||
|
||||
// 根据链接类型和成员信息生成替换内容
|
||||
switch link.Type {
|
||||
case "link_profile":
|
||||
// 使用自定义分隔符,如果未指定则默认使用"、"
|
||||
separator := link.Separator
|
||||
if separator == "" {
|
||||
separator = "、"
|
||||
}
|
||||
|
||||
// 处理成员信息,格式为 nickname(username)
|
||||
var memberTexts []string
|
||||
for _, member := range link.MemberList.Members {
|
||||
if member.Nickname != "" {
|
||||
memberText := member.Nickname
|
||||
if member.Username != "" {
|
||||
memberText += "(" + member.Username + ")"
|
||||
}
|
||||
memberTexts = append(memberTexts, memberText)
|
||||
}
|
||||
}
|
||||
|
||||
// 使用指定的分隔符连接所有成员文本
|
||||
replacement = strings.Join(memberTexts, separator)
|
||||
|
||||
// 可以根据需要添加其他链接类型的处理逻辑
|
||||
default:
|
||||
replacement = ""
|
||||
}
|
||||
|
||||
// 将占位符名称和替换内容存入映射
|
||||
replacements["$"+link.Name+"$"] = replacement
|
||||
}
|
||||
|
||||
// 使用正则表达式查找并替换所有占位符
|
||||
re := regexp.MustCompile(`\$([^$]+)\$`)
|
||||
result := re.ReplaceAllStringFunc(template, func(match string) string {
|
||||
if replacement, ok := replacements[match]; ok {
|
||||
return replacement
|
||||
}
|
||||
// 如果找不到对应的替换内容,保留原占位符
|
||||
return match
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
@@ -1,170 +1,215 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/model/wxproto"
|
||||
|
||||
"google.golang.org/protobuf/proto"
|
||||
"github.com/sjzar/chatlog/pkg/util"
|
||||
)
|
||||
|
||||
var Debug = false
|
||||
|
||||
const (
|
||||
// Source
|
||||
WeChatV3 = "wechatv3"
|
||||
WeChatV4 = "wechatv4"
|
||||
WeChatDarwinV3 = "wechatdarwinv3"
|
||||
)
|
||||
|
||||
type Message struct {
|
||||
Sequence int64 `json:"sequence"` // 消息序号,10位时间戳 + 3位序号
|
||||
CreateTime time.Time `json:"createTime"` // 消息创建时间,10位时间戳
|
||||
TalkerID int `json:"talkerID"` // 聊天对象,Name2ID 表序号,索引值
|
||||
Talker string `json:"talker"` // 聊天对象,微信 ID or 群 ID
|
||||
IsSender int `json:"isSender"` // 是否为发送消息,0 接收消息,1 发送消息
|
||||
Type int `json:"type"` // 消息类型
|
||||
SubType int `json:"subType"` // 消息子类型
|
||||
Content string `json:"content"` // 消息内容,文字聊天内容 或 XML
|
||||
CompressContent []byte `json:"compressContent"` // 非文字聊天内容,如图片、语音、视频等
|
||||
IsChatRoom bool `json:"isChatRoom"` // 是否为群聊消息
|
||||
ChatRoomSender string `json:"chatRoomSender"` // 群聊消息发送人
|
||||
|
||||
// Fill Info
|
||||
// 从联系人等信息中填充
|
||||
DisplayName string `json:"-"` // 显示名称
|
||||
ChatRoomName string `json:"-"` // 群聊名称
|
||||
|
||||
Version string `json:"-"` // 消息版本,内部判断
|
||||
Seq int64 `json:"seq"` // 消息序号,10位时间戳 + 3位序号
|
||||
Time time.Time `json:"time"` // 消息创建时间,10位时间戳
|
||||
Talker string `json:"talker"` // 聊天对象,微信 ID or 群 ID
|
||||
TalkerName string `json:"talkerName"` // 聊天对象名称
|
||||
IsChatRoom bool `json:"isChatRoom"` // 是否为群聊消息
|
||||
Sender string `json:"sender"` // 发送人,微信 ID
|
||||
SenderName string `json:"senderName"` // 发送人名称
|
||||
IsSelf bool `json:"isSelf"` // 是否为自己发送的消息
|
||||
Type int64 `json:"type"` // 消息类型
|
||||
SubType int64 `json:"subType"` // 消息子类型
|
||||
Content string `json:"content"` // 消息内容,文字聊天内容
|
||||
Contents map[string]interface{} `json:"contents,omitempty"` // 消息内容,多媒体消息,采用更灵活的记录方式
|
||||
|
||||
// Debug Info
|
||||
MediaMsg *MediaMsg `json:"mediaMsg,omitempty"` // 原始多媒体消息,XML 格式
|
||||
SysMsg *SysMsg `json:"sysMsg,omitempty"` // 原始系统消息,XML 格式
|
||||
}
|
||||
|
||||
// CREATE TABLE MSG (
|
||||
// localId INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
// TalkerId INT DEFAULT 0,
|
||||
// MsgSvrID INT,
|
||||
// Type INT,
|
||||
// SubType INT,
|
||||
// IsSender INT,
|
||||
// CreateTime INT,
|
||||
// Sequence INT DEFAULT 0,
|
||||
// StatusEx INT DEFAULT 0,
|
||||
// FlagEx INT,
|
||||
// Status INT,
|
||||
// MsgServerSeq INT,
|
||||
// MsgSequence INT,
|
||||
// StrTalker TEXT,
|
||||
// StrContent TEXT,
|
||||
// DisplayContent TEXT,
|
||||
// Reserved0 INT DEFAULT 0,
|
||||
// Reserved1 INT DEFAULT 0,
|
||||
// Reserved2 INT DEFAULT 0,
|
||||
// Reserved3 INT DEFAULT 0,
|
||||
// Reserved4 TEXT,
|
||||
// Reserved5 TEXT,
|
||||
// Reserved6 TEXT,
|
||||
// CompressContent BLOB,
|
||||
// BytesExtra BLOB,
|
||||
// BytesTrans BLOB
|
||||
// )
|
||||
type MessageV3 struct {
|
||||
Sequence int64 `json:"Sequence"` // 消息序号,10位时间戳 + 3位序号
|
||||
CreateTime int64 `json:"CreateTime"` // 消息创建时间,10位时间戳
|
||||
TalkerID int `json:"TalkerId"` // 聊天对象,Name2ID 表序号,索引值
|
||||
StrTalker string `json:"StrTalker"` // 聊天对象,微信 ID or 群 ID
|
||||
IsSender int `json:"IsSender"` // 是否为发送消息,0 接收消息,1 发送消息
|
||||
Type int `json:"Type"` // 消息类型
|
||||
SubType int `json:"SubType"` // 消息子类型
|
||||
StrContent string `json:"StrContent"` // 消息内容,文字聊天内容 或 XML
|
||||
CompressContent []byte `json:"CompressContent"` // 非文字聊天内容,如图片、语音、视频等
|
||||
BytesExtra []byte `json:"BytesExtra"` // protobuf 额外数据,记录群聊发送人等信息
|
||||
func (m *Message) ParseMediaInfo(data string) error {
|
||||
|
||||
// 非关键信息,后续有需要再加入
|
||||
// LocalID int64 `json:"localId"`
|
||||
// MsgSvrID int64 `json:"MsgSvrID"`
|
||||
// StatusEx int `json:"StatusEx"`
|
||||
// FlagEx int `json:"FlagEx"`
|
||||
// Status int `json:"Status"`
|
||||
// MsgServerSeq int64 `json:"MsgServerSeq"`
|
||||
// MsgSequence int64 `json:"MsgSequence"`
|
||||
// DisplayContent string `json:"DisplayContent"`
|
||||
// Reserved0 int `json:"Reserved0"`
|
||||
// Reserved1 int `json:"Reserved1"`
|
||||
// Reserved2 int `json:"Reserved2"`
|
||||
// Reserved3 int `json:"Reserved3"`
|
||||
// Reserved4 string `json:"Reserved4"`
|
||||
// Reserved5 string `json:"Reserved5"`
|
||||
// Reserved6 string `json:"Reserved6"`
|
||||
// BytesTrans []byte `json:"BytesTrans"`
|
||||
m.Type, m.SubType = util.SplitInt64ToTwoInt32(m.Type)
|
||||
|
||||
if m.Type == 1 {
|
||||
m.Content = data
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MessageV3) Wrap() *Message {
|
||||
|
||||
isChatRoom := strings.HasSuffix(m.StrTalker, "@chatroom")
|
||||
|
||||
var chatRoomSender string
|
||||
if len(m.BytesExtra) != 0 && isChatRoom {
|
||||
chatRoomSender = ParseBytesExtra(m.BytesExtra)
|
||||
if m.Type == 10000 {
|
||||
var sysMsg SysMsg
|
||||
if err := xml.Unmarshal([]byte(data), &sysMsg); err != nil {
|
||||
m.Content = data
|
||||
return nil
|
||||
}
|
||||
if Debug {
|
||||
m.SysMsg = &sysMsg
|
||||
}
|
||||
m.Content = sysMsg.String()
|
||||
return nil
|
||||
}
|
||||
|
||||
return &Message{
|
||||
Sequence: m.Sequence,
|
||||
CreateTime: time.Unix(m.CreateTime, 0),
|
||||
TalkerID: m.TalkerID,
|
||||
Talker: m.StrTalker,
|
||||
IsSender: m.IsSender,
|
||||
Type: m.Type,
|
||||
SubType: m.SubType,
|
||||
Content: m.StrContent,
|
||||
CompressContent: m.CompressContent,
|
||||
IsChatRoom: isChatRoom,
|
||||
ChatRoomSender: chatRoomSender,
|
||||
Version: WeChatV3,
|
||||
var msg MediaMsg
|
||||
err := xml.Unmarshal([]byte(data), &msg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if m.Contents == nil {
|
||||
m.Contents = make(map[string]interface{})
|
||||
}
|
||||
|
||||
if Debug {
|
||||
m.MediaMsg = &msg
|
||||
}
|
||||
|
||||
switch m.Type {
|
||||
case 3:
|
||||
m.Contents["md5"] = msg.Image.MD5
|
||||
case 43:
|
||||
m.Contents["md5"] = msg.Video.RawMd5
|
||||
case 49:
|
||||
m.SubType = int64(msg.App.Type)
|
||||
switch m.SubType {
|
||||
case 5:
|
||||
// 链接
|
||||
m.Contents["title"] = msg.App.Title
|
||||
m.Contents["url"] = msg.App.URL
|
||||
case 6:
|
||||
// 文件
|
||||
m.Contents["title"] = msg.App.Title
|
||||
m.Contents["md5"] = msg.App.MD5
|
||||
case 19:
|
||||
// 合并转发
|
||||
m.Contents["title"] = msg.App.Title
|
||||
m.Contents["desc"] = msg.App.Des
|
||||
if msg.App.RecordItem == nil {
|
||||
break
|
||||
}
|
||||
recordInfo := &RecordInfo{}
|
||||
err := xml.Unmarshal([]byte(msg.App.RecordItem.CDATA), recordInfo)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
m.Contents["recordInfo"] = recordInfo
|
||||
case 33, 36:
|
||||
// 小程序
|
||||
m.Contents["title"] = msg.App.SourceDisplayName
|
||||
m.Contents["url"] = msg.App.URL
|
||||
case 51:
|
||||
// 视频号
|
||||
if msg.App.FinderFeed == nil {
|
||||
break
|
||||
}
|
||||
m.Contents["title"] = msg.App.FinderFeed.Desc
|
||||
if len(msg.App.FinderFeed.MediaList.Media) > 0 {
|
||||
m.Contents["url"] = msg.App.FinderFeed.MediaList.Media[0].URL
|
||||
}
|
||||
case 57:
|
||||
// 引用
|
||||
m.Content = msg.App.Title
|
||||
if msg.App.ReferMsg == nil {
|
||||
break
|
||||
}
|
||||
subMsg := &Message{
|
||||
Type: int64(msg.App.ReferMsg.Type),
|
||||
Time: time.Unix(msg.App.ReferMsg.CreateTime, 0),
|
||||
Sender: msg.App.ReferMsg.ChatUsr,
|
||||
SenderName: msg.App.ReferMsg.DisplayName,
|
||||
}
|
||||
if subMsg.Sender == "" {
|
||||
subMsg.Sender = msg.App.ReferMsg.FromUsr
|
||||
}
|
||||
if err := subMsg.ParseMediaInfo(msg.App.ReferMsg.Content); err != nil {
|
||||
break
|
||||
}
|
||||
m.Contents["refer"] = subMsg
|
||||
case 62:
|
||||
// 拍一拍
|
||||
if msg.App.PatMsg == nil {
|
||||
break
|
||||
}
|
||||
if len(msg.App.PatMsg.Records.Record) == 0 {
|
||||
break
|
||||
}
|
||||
m.Sender = msg.App.PatMsg.Records.Record[0].FromUser
|
||||
m.Content = msg.App.PatMsg.Records.Record[0].Templete
|
||||
case 2000:
|
||||
// 微信转账
|
||||
if msg.App.WCPayInfo == nil {
|
||||
break
|
||||
}
|
||||
// 1 实时转账
|
||||
// 3 实时转账收钱回执
|
||||
// 4 转账退还回执
|
||||
// 5 非实时转账收钱回执
|
||||
// 7 非实时转账
|
||||
_type := ""
|
||||
switch msg.App.WCPayInfo.PaySubType {
|
||||
case 1, 7:
|
||||
_type = "发送 "
|
||||
case 3, 5:
|
||||
_type = "接收 "
|
||||
case 4:
|
||||
_type = "退还 "
|
||||
}
|
||||
payMemo := ""
|
||||
if len(msg.App.WCPayInfo.PayMemo) > 0 {
|
||||
payMemo = "(" + msg.App.WCPayInfo.PayMemo + ")"
|
||||
}
|
||||
m.Content = fmt.Sprintf("[转账|%s%s]%s", _type, msg.App.WCPayInfo.FeeDesc, payMemo)
|
||||
}
|
||||
}
|
||||
|
||||
// ParseBytesExtra 解析额外数据
|
||||
// 按需解析
|
||||
func ParseBytesExtra(b []byte) (chatRoomSender string) {
|
||||
var pbMsg wxproto.BytesExtra
|
||||
if err := proto.Unmarshal(b, &pbMsg); err != nil {
|
||||
return
|
||||
}
|
||||
if pbMsg.Items == nil {
|
||||
return
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, item := range pbMsg.Items {
|
||||
if item.Type == 1 {
|
||||
return item.Value
|
||||
func (m *Message) SetContent(key string, value interface{}) {
|
||||
if m.Contents == nil {
|
||||
m.Contents = make(map[string]interface{})
|
||||
}
|
||||
m.Contents[key] = value
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
func (m *Message) PlainText(showChatRoom bool, host string) string {
|
||||
|
||||
m.SetContent("host", host)
|
||||
|
||||
func (m *Message) PlainText(showChatRoom bool) string {
|
||||
buf := strings.Builder{}
|
||||
|
||||
talker := m.Talker
|
||||
if m.IsSender == 1 {
|
||||
talker = "我"
|
||||
} else if m.IsChatRoom {
|
||||
talker = m.ChatRoomSender
|
||||
sender := m.Sender
|
||||
switch {
|
||||
case m.Type == 10000:
|
||||
sender = "系统消息"
|
||||
case m.IsSelf:
|
||||
sender = "我"
|
||||
default:
|
||||
sender = m.Sender
|
||||
}
|
||||
if m.DisplayName != "" {
|
||||
buf.WriteString(m.DisplayName)
|
||||
if m.SenderName != "" {
|
||||
buf.WriteString(m.SenderName)
|
||||
buf.WriteString("(")
|
||||
buf.WriteString(talker)
|
||||
buf.WriteString(sender)
|
||||
buf.WriteString(")")
|
||||
} else {
|
||||
buf.WriteString(talker)
|
||||
buf.WriteString(sender)
|
||||
}
|
||||
buf.WriteString(" ")
|
||||
|
||||
if m.IsChatRoom && showChatRoom {
|
||||
buf.WriteString("[")
|
||||
if m.ChatRoomName != "" {
|
||||
buf.WriteString(m.ChatRoomName)
|
||||
if m.TalkerName != "" {
|
||||
buf.WriteString(m.TalkerName)
|
||||
buf.WriteString("(")
|
||||
buf.WriteString(m.Talker)
|
||||
buf.WriteString(")")
|
||||
@@ -174,55 +219,112 @@ func (m *Message) PlainText(showChatRoom bool) string {
|
||||
buf.WriteString("] ")
|
||||
}
|
||||
|
||||
buf.WriteString(m.CreateTime.Format("2006-01-02 15:04:05"))
|
||||
buf.WriteString(m.Time.Format("2006-01-02 15:04:05"))
|
||||
buf.WriteString("\n")
|
||||
|
||||
buf.WriteString(m.PlainTextContent())
|
||||
buf.WriteString("\n")
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
func (m *Message) PlainTextContent() string {
|
||||
switch m.Type {
|
||||
case 1:
|
||||
buf.WriteString(m.Content)
|
||||
return m.Content
|
||||
case 3:
|
||||
buf.WriteString("[图片]")
|
||||
return fmt.Sprintf("", m.Contents["host"], m.Contents["md5"])
|
||||
case 34:
|
||||
buf.WriteString("[语音]")
|
||||
return "[语音]"
|
||||
case 42:
|
||||
return "[名片]"
|
||||
case 43:
|
||||
buf.WriteString("[视频]")
|
||||
if path, ok := m.Contents["path"]; ok {
|
||||
return fmt.Sprintf("", m.Contents["host"], path)
|
||||
}
|
||||
return fmt.Sprintf("", m.Contents["host"], m.Contents["md5"])
|
||||
case 47:
|
||||
buf.WriteString("[动画表情]")
|
||||
return "[动画表情]"
|
||||
case 49:
|
||||
switch m.SubType {
|
||||
case 5:
|
||||
return fmt.Sprintf("[链接|%s](%s)", m.Contents["title"], m.Contents["url"])
|
||||
case 6:
|
||||
buf.WriteString("[文件]")
|
||||
return fmt.Sprintf("[文件|%s](http://%s/file/%s)", m.Contents["title"], m.Contents["host"], m.Contents["md5"])
|
||||
case 8:
|
||||
buf.WriteString("[GIF表情]")
|
||||
return "[GIF表情]"
|
||||
case 19:
|
||||
buf.WriteString("[合并转发]")
|
||||
_recordInfo, ok := m.Contents["recordInfo"]
|
||||
if !ok {
|
||||
return "[合并转发]"
|
||||
}
|
||||
recordInfo, ok := _recordInfo.(*RecordInfo)
|
||||
if !ok {
|
||||
return "[合并转发]"
|
||||
}
|
||||
return recordInfo.String("", m.Contents["host"].(string))
|
||||
case 33, 36:
|
||||
buf.WriteString("[小程序]")
|
||||
if m.Contents["title"] == "" {
|
||||
return "[小程序]"
|
||||
}
|
||||
return fmt.Sprintf("[小程序|%s](%s)", m.Contents["title"], m.Contents["url"])
|
||||
case 51:
|
||||
if m.Contents["title"] == "" {
|
||||
return "[视频号]"
|
||||
} else {
|
||||
return fmt.Sprintf("[视频号|%s](%s)", m.Contents["title"], m.Contents["url"])
|
||||
}
|
||||
case 57:
|
||||
buf.WriteString("[引用]")
|
||||
_refer, ok := m.Contents["refer"]
|
||||
if !ok {
|
||||
if m.Content == "" {
|
||||
return "[引用]"
|
||||
}
|
||||
return "> [引用]\n" + m.Content
|
||||
}
|
||||
refer, ok := _refer.(*Message)
|
||||
if !ok {
|
||||
if m.Content == "" {
|
||||
return "[引用]"
|
||||
}
|
||||
return "> [引用]\n" + m.Content
|
||||
}
|
||||
buf := strings.Builder{}
|
||||
referContent := refer.PlainText(false, m.Contents["host"].(string))
|
||||
for _, line := range strings.Split(referContent, "\n") {
|
||||
if line == "" {
|
||||
continue
|
||||
}
|
||||
buf.WriteString("> ")
|
||||
buf.WriteString(line)
|
||||
buf.WriteString("\n")
|
||||
}
|
||||
buf.WriteString(m.Content)
|
||||
return buf.String()
|
||||
case 62:
|
||||
return m.Content
|
||||
case 63:
|
||||
buf.WriteString("[视频号]")
|
||||
return "[视频号]"
|
||||
case 87:
|
||||
buf.WriteString("[群公告]")
|
||||
return "[群公告]"
|
||||
case 2000:
|
||||
buf.WriteString("[转账]")
|
||||
return m.Content
|
||||
case 2001:
|
||||
return "[红包]"
|
||||
case 2003:
|
||||
buf.WriteString("[红包封面]")
|
||||
return "[红包封面]"
|
||||
default:
|
||||
buf.WriteString("[分享]")
|
||||
return "[分享]"
|
||||
}
|
||||
case 50:
|
||||
buf.WriteString("[语音通话]")
|
||||
return "[语音通话]"
|
||||
case 10000:
|
||||
buf.WriteString("[系统消息]")
|
||||
return m.Content
|
||||
default:
|
||||
content := m.Content
|
||||
if len(content) > 120 {
|
||||
content = content[:120] + "<...>"
|
||||
}
|
||||
buf.WriteString(fmt.Sprintf("Type: %d Content: %s", m.Type, content))
|
||||
return fmt.Sprintf("Type: %d Content: %s", m.Type, content)
|
||||
}
|
||||
buf.WriteString("\n")
|
||||
|
||||
return buf.String()
|
||||
}
|
||||
|
||||
@@ -23,47 +23,35 @@ import (
|
||||
// ConBlob BLOB
|
||||
// )
|
||||
type MessageDarwinV3 struct {
|
||||
MesCreateTime int64 `json:"mesCreateTime"`
|
||||
MesContent string `json:"mesContent"`
|
||||
MesType int `json:"mesType"`
|
||||
MsgCreateTime int64 `json:"msgCreateTime"`
|
||||
MsgContent string `json:"msgContent"`
|
||||
MessageType int64 `json:"messageType"`
|
||||
MesDes int `json:"mesDes"` // 0: 发送, 1: 接收
|
||||
MesSource string `json:"mesSource"`
|
||||
|
||||
// MesLocalID int64 `json:"mesLocalID"`
|
||||
// MesSvrID int64 `json:"mesSvrID"`
|
||||
// MesStatus int `json:"mesStatus"`
|
||||
// MesImgStatus int `json:"mesImgStatus"`
|
||||
// IntRes1 int `json:"IntRes1"`
|
||||
// IntRes2 int `json:"IntRes2"`
|
||||
// StrRes1 string `json:"StrRes1"`
|
||||
// StrRes2 string `json:"StrRes2"`
|
||||
// MesVoiceText string `json:"mesVoiceText"`
|
||||
// MesSeq int `json:"mesSeq"`
|
||||
// CompressContent []byte `json:"CompressContent"`
|
||||
// ConBlob []byte `json:"ConBlob"`
|
||||
}
|
||||
|
||||
func (m *MessageDarwinV3) Wrap(talker string) *Message {
|
||||
isChatRoom := strings.HasSuffix(talker, "@chatroom")
|
||||
|
||||
var chatRoomSender string
|
||||
content := m.MesContent
|
||||
if isChatRoom {
|
||||
split := strings.SplitN(m.MesContent, ":\n", 2)
|
||||
if len(split) == 2 {
|
||||
chatRoomSender = split[0]
|
||||
content = split[1]
|
||||
}
|
||||
}
|
||||
|
||||
return &Message{
|
||||
CreateTime: time.Unix(m.MesCreateTime, 0),
|
||||
Content: content,
|
||||
_m := &Message{
|
||||
Time: time.Unix(m.MsgCreateTime, 0),
|
||||
Type: m.MessageType,
|
||||
Talker: talker,
|
||||
Type: m.MesType,
|
||||
IsSender: (m.MesDes + 1) % 2,
|
||||
IsChatRoom: isChatRoom,
|
||||
ChatRoomSender: chatRoomSender,
|
||||
IsChatRoom: strings.HasSuffix(talker, "@chatroom"),
|
||||
IsSelf: m.MesDes == 0,
|
||||
Version: WeChatDarwinV3,
|
||||
}
|
||||
|
||||
content := m.MsgContent
|
||||
if _m.IsChatRoom {
|
||||
split := strings.SplitN(content, ":\n", 2)
|
||||
if len(split) == 2 {
|
||||
_m.Sender = split[0]
|
||||
content = split[1]
|
||||
}
|
||||
} else if !_m.IsSelf {
|
||||
_m.Sender = talker
|
||||
}
|
||||
|
||||
_m.ParseMediaInfo(content)
|
||||
|
||||
return _m
|
||||
}
|
||||
|
||||
117
internal/model/message_v3.go
Normal file
117
internal/model/message_v3.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/model/wxproto"
|
||||
"github.com/sjzar/chatlog/pkg/util/lz4"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// CREATE TABLE MSG (
|
||||
// localId INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
// TalkerId INT DEFAULT 0,
|
||||
// MsgSvrID INT,
|
||||
// Type INT,
|
||||
// SubType INT,
|
||||
// IsSender INT,
|
||||
// CreateTime INT,
|
||||
// Sequence INT DEFAULT 0,
|
||||
// StatusEx INT DEFAULT 0,
|
||||
// FlagEx INT,
|
||||
// Status INT,
|
||||
// MsgServerSeq INT,
|
||||
// MsgSequence INT,
|
||||
// StrTalker TEXT,
|
||||
// StrContent TEXT,
|
||||
// DisplayContent TEXT,
|
||||
// Reserved0 INT DEFAULT 0,
|
||||
// Reserved1 INT DEFAULT 0,
|
||||
// Reserved2 INT DEFAULT 0,
|
||||
// Reserved3 INT DEFAULT 0,
|
||||
// Reserved4 TEXT,
|
||||
// Reserved5 TEXT,
|
||||
// Reserved6 TEXT,
|
||||
// CompressContent BLOB,
|
||||
// BytesExtra BLOB,
|
||||
// BytesTrans BLOB
|
||||
// )
|
||||
type MessageV3 struct {
|
||||
Sequence int64 `json:"Sequence"` // 消息序号,10位时间戳 + 3位序号
|
||||
CreateTime int64 `json:"CreateTime"` // 消息创建时间,10位时间戳
|
||||
StrTalker string `json:"StrTalker"` // 聊天对象,微信 ID or 群 ID
|
||||
IsSender int `json:"IsSender"` // 是否为发送消息,0 接收消息,1 发送消息
|
||||
Type int64 `json:"Type"` // 消息类型
|
||||
SubType int `json:"SubType"` // 消息子类型
|
||||
StrContent string `json:"StrContent"` // 消息内容,文字聊天内容 或 XML
|
||||
CompressContent []byte `json:"CompressContent"` // 非文字聊天内容,如图片、语音、视频等
|
||||
BytesExtra []byte `json:"BytesExtra"` // protobuf 额外数据,记录群聊发送人等信息
|
||||
}
|
||||
|
||||
func (m *MessageV3) Wrap() *Message {
|
||||
|
||||
_m := &Message{
|
||||
Seq: m.Sequence,
|
||||
Time: time.Unix(m.CreateTime, 0),
|
||||
Talker: m.StrTalker,
|
||||
IsChatRoom: strings.HasSuffix(m.StrTalker, "@chatroom"),
|
||||
IsSelf: m.IsSender == 1,
|
||||
Type: m.Type,
|
||||
SubType: int64(m.SubType),
|
||||
Content: m.StrContent,
|
||||
Version: WeChatV3,
|
||||
}
|
||||
|
||||
if !_m.IsChatRoom && !_m.IsSelf {
|
||||
_m.Sender = m.StrTalker
|
||||
}
|
||||
|
||||
if _m.Type == 49 {
|
||||
b, err := lz4.Decompress(m.CompressContent)
|
||||
if err == nil {
|
||||
_m.Content = string(b)
|
||||
}
|
||||
}
|
||||
|
||||
_m.ParseMediaInfo(_m.Content)
|
||||
|
||||
if len(m.BytesExtra) != 0 {
|
||||
if bytesExtra := ParseBytesExtra(m.BytesExtra); bytesExtra != nil {
|
||||
if _m.IsChatRoom {
|
||||
_m.Sender = bytesExtra[1]
|
||||
}
|
||||
// FIXME xml 中的 md5 数据无法匹配到 hardlink 记录,所以直接用 proto 数据
|
||||
if _m.Type == 43 {
|
||||
path := bytesExtra[4]
|
||||
parts := strings.Split(filepath.ToSlash(path), "/")
|
||||
if len(parts) > 1 {
|
||||
path = strings.Join(parts[1:], "/")
|
||||
}
|
||||
_m.Contents["path"] = path
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _m
|
||||
}
|
||||
|
||||
// ParseBytesExtra 解析额外数据
|
||||
// 按需解析
|
||||
func ParseBytesExtra(b []byte) map[int]string {
|
||||
var pbMsg wxproto.BytesExtra
|
||||
if err := proto.Unmarshal(b, &pbMsg); err != nil {
|
||||
return nil
|
||||
}
|
||||
if pbMsg.Items == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
ret := make(map[int]string, len(pbMsg.Items))
|
||||
for _, item := range pbMsg.Items {
|
||||
ret[int(item.Type)] = item.Value
|
||||
}
|
||||
|
||||
return ret
|
||||
}
|
||||
@@ -5,7 +5,9 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/model/wxproto"
|
||||
"github.com/sjzar/chatlog/pkg/util/zstd"
|
||||
"google.golang.org/protobuf/proto"
|
||||
)
|
||||
|
||||
// CREATE TABLE Msg_md5(talker)(
|
||||
@@ -29,63 +31,68 @@ import (
|
||||
// )
|
||||
type MessageV4 struct {
|
||||
SortSeq int64 `json:"sort_seq"` // 消息序号,10位时间戳 + 3位序号
|
||||
LocalType int `json:"local_type"` // 消息类型
|
||||
RealSenderID int `json:"real_sender_id"` // 发送人 ID,对应 Name2Id 表序号
|
||||
LocalType int64 `json:"local_type"` // 消息类型
|
||||
UserName string `json:"user_name"` // 发送人,通过 Join Name2Id 表获得
|
||||
CreateTime int64 `json:"create_time"` // 消息创建时间,10位时间戳
|
||||
MessageContent []byte `json:"message_content"` // 消息内容,文字聊天内容 或 zstd 压缩内容
|
||||
PackedInfoData []byte `json:"packed_info_data"` // 额外数据,类似 proto,格式与 v3 有差异
|
||||
Status int `json:"status"` // 消息状态,2 是已发送,4 是已接收,可以用于判断 IsSender(猜测)
|
||||
|
||||
// 非关键信息,后续有需要再加入
|
||||
// LocalID int `json:"local_id"`
|
||||
// ServerID int64 `json:"server_id"`
|
||||
// UploadStatus int `json:"upload_status"`
|
||||
// DownloadStatus int `json:"download_status"`
|
||||
// ServerSeq int `json:"server_seq"`
|
||||
// OriginSource int `json:"origin_source"`
|
||||
// Source string `json:"source"`
|
||||
// CompressContent string `json:"compress_content"`
|
||||
Status int `json:"status"` // 消息状态,2 是已发送,4 是已接收,可以用于判断 IsSender(FIXME 不准, 需要判断 UserName)
|
||||
}
|
||||
|
||||
func (m *MessageV4) Wrap(id2Name map[int]string, isChatRoom bool) *Message {
|
||||
func (m *MessageV4) Wrap(talker string) *Message {
|
||||
|
||||
_m := &Message{
|
||||
Sequence: m.SortSeq,
|
||||
CreateTime: time.Unix(m.CreateTime, 0),
|
||||
TalkerID: m.RealSenderID, // 依赖 Name2Id 表进行转换为 StrTalker
|
||||
CompressContent: m.PackedInfoData,
|
||||
Seq: m.SortSeq,
|
||||
Time: time.Unix(m.CreateTime, 0),
|
||||
Talker: talker,
|
||||
IsChatRoom: strings.HasSuffix(talker, "@chatroom"),
|
||||
Sender: m.UserName,
|
||||
Type: m.LocalType,
|
||||
Contents: make(map[string]interface{}),
|
||||
Version: WeChatV4,
|
||||
}
|
||||
|
||||
if name, ok := id2Name[m.RealSenderID]; ok {
|
||||
_m.Talker = name
|
||||
}
|
||||
// FIXME 后续通过 UserName 判断是否是自己发送的消息,目前可能不准确
|
||||
_m.IsSelf = m.Status == 2 || (!_m.IsChatRoom && talker != m.UserName)
|
||||
|
||||
if m.Status == 2 {
|
||||
_m.IsSender = 1
|
||||
}
|
||||
|
||||
if _m.Type == 1 {
|
||||
_m.Content = string(m.MessageContent)
|
||||
} else {
|
||||
content := ""
|
||||
if bytes.HasPrefix(m.MessageContent, []byte{0x28, 0xb5, 0x2f, 0xfd}) {
|
||||
if b, err := zstd.Decompress(m.MessageContent); err == nil {
|
||||
_m.Content = string(b)
|
||||
content = string(b)
|
||||
}
|
||||
} else {
|
||||
_m.CompressContent = m.MessageContent
|
||||
content = string(m.MessageContent)
|
||||
}
|
||||
|
||||
if _m.IsChatRoom {
|
||||
split := strings.SplitN(content, ":\n", 2)
|
||||
if len(split) == 2 {
|
||||
_m.Sender = split[0]
|
||||
content = split[1]
|
||||
}
|
||||
}
|
||||
|
||||
if isChatRoom {
|
||||
_m.IsChatRoom = true
|
||||
split := strings.SplitN(_m.Content, ":\n", 2)
|
||||
if len(split) == 2 {
|
||||
_m.ChatRoomSender = split[0]
|
||||
_m.Content = split[1]
|
||||
_m.ParseMediaInfo(content)
|
||||
|
||||
if len(m.PackedInfoData) != 0 {
|
||||
if packedInfo := ParsePackedInfo(m.PackedInfoData); packedInfo != nil {
|
||||
// FIXME 尝试解决 v4 版本 xml 数据无法匹配到 hardlink 记录的问题
|
||||
if _m.Type == 3 && packedInfo.Image != nil {
|
||||
_m.Contents["md5"] = packedInfo.Image.Md5
|
||||
}
|
||||
if _m.Type == 43 && packedInfo.Video != nil {
|
||||
_m.Contents["md5"] = packedInfo.Video.Md5
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return _m
|
||||
}
|
||||
|
||||
func ParsePackedInfo(b []byte) *wxproto.PackedInfo {
|
||||
var pbMsg wxproto.PackedInfo
|
||||
if err := proto.Unmarshal(b, &pbMsg); err != nil {
|
||||
return nil
|
||||
}
|
||||
return &pbMsg
|
||||
}
|
||||
|
||||
252
internal/model/wxproto/packedinfo.pb.go
Normal file
252
internal/model/wxproto/packedinfo.pb.go
Normal file
@@ -0,0 +1,252 @@
|
||||
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||
// versions:
|
||||
// protoc-gen-go v1.36.5
|
||||
// protoc v5.29.3
|
||||
// source: packedinfo.proto
|
||||
|
||||
package wxproto
|
||||
|
||||
import (
|
||||
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||
reflect "reflect"
|
||||
sync "sync"
|
||||
unsafe "unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
// Verify that this generated code is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||
)
|
||||
|
||||
type PackedInfo struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Type uint32 `protobuf:"varint,1,opt,name=type,proto3" json:"type,omitempty"` // 始终为 106 (0x6a)
|
||||
Version uint32 `protobuf:"varint,2,opt,name=version,proto3" json:"version,omitempty"` // 始终为 14 (0xe)
|
||||
Image *ImageHash `protobuf:"bytes,3,opt,name=image,proto3" json:"image,omitempty"` // 图片哈希
|
||||
Video *VideoHash `protobuf:"bytes,4,opt,name=video,proto3" json:"video,omitempty"` // 视频哈希
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *PackedInfo) Reset() {
|
||||
*x = PackedInfo{}
|
||||
mi := &file_packedinfo_proto_msgTypes[0]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *PackedInfo) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*PackedInfo) ProtoMessage() {}
|
||||
|
||||
func (x *PackedInfo) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_packedinfo_proto_msgTypes[0]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use PackedInfo.ProtoReflect.Descriptor instead.
|
||||
func (*PackedInfo) Descriptor() ([]byte, []int) {
|
||||
return file_packedinfo_proto_rawDescGZIP(), []int{0}
|
||||
}
|
||||
|
||||
func (x *PackedInfo) GetType() uint32 {
|
||||
if x != nil {
|
||||
return x.Type
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *PackedInfo) GetVersion() uint32 {
|
||||
if x != nil {
|
||||
return x.Version
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (x *PackedInfo) GetImage() *ImageHash {
|
||||
if x != nil {
|
||||
return x.Image
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (x *PackedInfo) GetVideo() *VideoHash {
|
||||
if x != nil {
|
||||
return x.Video
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type ImageHash struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Md5 string `protobuf:"bytes,4,opt,name=md5,proto3" json:"md5,omitempty"` // 32 字符的 MD5 哈希
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ImageHash) Reset() {
|
||||
*x = ImageHash{}
|
||||
mi := &file_packedinfo_proto_msgTypes[1]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *ImageHash) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*ImageHash) ProtoMessage() {}
|
||||
|
||||
func (x *ImageHash) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_packedinfo_proto_msgTypes[1]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use ImageHash.ProtoReflect.Descriptor instead.
|
||||
func (*ImageHash) Descriptor() ([]byte, []int) {
|
||||
return file_packedinfo_proto_rawDescGZIP(), []int{1}
|
||||
}
|
||||
|
||||
func (x *ImageHash) GetMd5() string {
|
||||
if x != nil {
|
||||
return x.Md5
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
type VideoHash struct {
|
||||
state protoimpl.MessageState `protogen:"open.v1"`
|
||||
Md5 string `protobuf:"bytes,8,opt,name=md5,proto3" json:"md5,omitempty"` // 32 字符的 MD5 哈希
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *VideoHash) Reset() {
|
||||
*x = VideoHash{}
|
||||
mi := &file_packedinfo_proto_msgTypes[2]
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
|
||||
func (x *VideoHash) String() string {
|
||||
return protoimpl.X.MessageStringOf(x)
|
||||
}
|
||||
|
||||
func (*VideoHash) ProtoMessage() {}
|
||||
|
||||
func (x *VideoHash) ProtoReflect() protoreflect.Message {
|
||||
mi := &file_packedinfo_proto_msgTypes[2]
|
||||
if x != nil {
|
||||
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||
if ms.LoadMessageInfo() == nil {
|
||||
ms.StoreMessageInfo(mi)
|
||||
}
|
||||
return ms
|
||||
}
|
||||
return mi.MessageOf(x)
|
||||
}
|
||||
|
||||
// Deprecated: Use VideoHash.ProtoReflect.Descriptor instead.
|
||||
func (*VideoHash) Descriptor() ([]byte, []int) {
|
||||
return file_packedinfo_proto_rawDescGZIP(), []int{2}
|
||||
}
|
||||
|
||||
func (x *VideoHash) GetMd5() string {
|
||||
if x != nil {
|
||||
return x.Md5
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
var File_packedinfo_proto protoreflect.FileDescriptor
|
||||
|
||||
var file_packedinfo_proto_rawDesc = string([]byte{
|
||||
0x0a, 0x10, 0x70, 0x61, 0x63, 0x6b, 0x65, 0x64, 0x69, 0x6e, 0x66, 0x6f, 0x2e, 0x70, 0x72, 0x6f,
|
||||
0x74, 0x6f, 0x12, 0x0c, 0x61, 0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66,
|
||||
0x22, 0x98, 0x01, 0x0a, 0x0a, 0x50, 0x61, 0x63, 0x6b, 0x65, 0x64, 0x49, 0x6e, 0x66, 0x6f, 0x12,
|
||||
0x12, 0x0a, 0x04, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x04, 0x74,
|
||||
0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x02,
|
||||
0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x2d, 0x0a,
|
||||
0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x61,
|
||||
0x70, 0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x49, 0x6d, 0x61, 0x67,
|
||||
0x65, 0x48, 0x61, 0x73, 0x68, 0x52, 0x05, 0x69, 0x6d, 0x61, 0x67, 0x65, 0x12, 0x2d, 0x0a, 0x05,
|
||||
0x76, 0x69, 0x64, 0x65, 0x6f, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x17, 0x2e, 0x61, 0x70,
|
||||
0x70, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x56, 0x69, 0x64, 0x65, 0x6f,
|
||||
0x48, 0x61, 0x73, 0x68, 0x52, 0x05, 0x76, 0x69, 0x64, 0x65, 0x6f, 0x22, 0x1d, 0x0a, 0x09, 0x49,
|
||||
0x6d, 0x61, 0x67, 0x65, 0x48, 0x61, 0x73, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x64, 0x35, 0x18,
|
||||
0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x64, 0x35, 0x22, 0x1d, 0x0a, 0x09, 0x56, 0x69,
|
||||
0x64, 0x65, 0x6f, 0x48, 0x61, 0x73, 0x68, 0x12, 0x10, 0x0a, 0x03, 0x6d, 0x64, 0x35, 0x18, 0x08,
|
||||
0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x64, 0x35, 0x42, 0x0b, 0x5a, 0x09, 0x2e, 0x3b, 0x77,
|
||||
0x78, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||
})
|
||||
|
||||
var (
|
||||
file_packedinfo_proto_rawDescOnce sync.Once
|
||||
file_packedinfo_proto_rawDescData []byte
|
||||
)
|
||||
|
||||
func file_packedinfo_proto_rawDescGZIP() []byte {
|
||||
file_packedinfo_proto_rawDescOnce.Do(func() {
|
||||
file_packedinfo_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_packedinfo_proto_rawDesc), len(file_packedinfo_proto_rawDesc)))
|
||||
})
|
||||
return file_packedinfo_proto_rawDescData
|
||||
}
|
||||
|
||||
var file_packedinfo_proto_msgTypes = make([]protoimpl.MessageInfo, 3)
|
||||
var file_packedinfo_proto_goTypes = []any{
|
||||
(*PackedInfo)(nil), // 0: app.protobuf.PackedInfo
|
||||
(*ImageHash)(nil), // 1: app.protobuf.ImageHash
|
||||
(*VideoHash)(nil), // 2: app.protobuf.VideoHash
|
||||
}
|
||||
var file_packedinfo_proto_depIdxs = []int32{
|
||||
1, // 0: app.protobuf.PackedInfo.image:type_name -> app.protobuf.ImageHash
|
||||
2, // 1: app.protobuf.PackedInfo.video:type_name -> app.protobuf.VideoHash
|
||||
2, // [2:2] is the sub-list for method output_type
|
||||
2, // [2:2] is the sub-list for method input_type
|
||||
2, // [2:2] is the sub-list for extension type_name
|
||||
2, // [2:2] is the sub-list for extension extendee
|
||||
0, // [0:2] is the sub-list for field type_name
|
||||
}
|
||||
|
||||
func init() { file_packedinfo_proto_init() }
|
||||
func file_packedinfo_proto_init() {
|
||||
if File_packedinfo_proto != nil {
|
||||
return
|
||||
}
|
||||
type x struct{}
|
||||
out := protoimpl.TypeBuilder{
|
||||
File: protoimpl.DescBuilder{
|
||||
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||
RawDescriptor: unsafe.Slice(unsafe.StringData(file_packedinfo_proto_rawDesc), len(file_packedinfo_proto_rawDesc)),
|
||||
NumEnums: 0,
|
||||
NumMessages: 3,
|
||||
NumExtensions: 0,
|
||||
NumServices: 0,
|
||||
},
|
||||
GoTypes: file_packedinfo_proto_goTypes,
|
||||
DependencyIndexes: file_packedinfo_proto_depIdxs,
|
||||
MessageInfos: file_packedinfo_proto_msgTypes,
|
||||
}.Build()
|
||||
File_packedinfo_proto = out.File
|
||||
file_packedinfo_proto_goTypes = nil
|
||||
file_packedinfo_proto_depIdxs = nil
|
||||
}
|
||||
19
internal/model/wxproto/packedinfo.proto
Normal file
19
internal/model/wxproto/packedinfo.proto
Normal file
@@ -0,0 +1,19 @@
|
||||
syntax = "proto3";
|
||||
package app.protobuf;
|
||||
option go_package=".;wxproto";
|
||||
|
||||
message PackedInfo {
|
||||
uint32 type = 1; // 始终为 106 (0x6a)
|
||||
uint32 version = 2; // 始终为 14 (0xe)
|
||||
ImageHash image = 3; // 图片哈希
|
||||
VideoHash video = 4; // 视频哈希
|
||||
}
|
||||
|
||||
|
||||
message ImageHash {
|
||||
string md5 = 4; // 32 字符的 MD5 哈希
|
||||
}
|
||||
|
||||
message VideoHash {
|
||||
string md5 = 8; // 32 字符的 MD5 哈希
|
||||
}
|
||||
@@ -32,13 +32,13 @@ type DBFile struct {
|
||||
func OpenDBFile(dbPath string, pageSize int) (*DBFile, error) {
|
||||
fp, err := os.Open(dbPath)
|
||||
if err != nil {
|
||||
return nil, errors.DecryptOpenFileFailed(dbPath, err)
|
||||
return nil, errors.OpenFileFailed(dbPath, err)
|
||||
}
|
||||
defer fp.Close()
|
||||
|
||||
fileInfo, err := fp.Stat()
|
||||
if err != nil {
|
||||
return nil, errors.WeChatDecryptFailed(err)
|
||||
return nil, errors.StatFileFailed(dbPath, err)
|
||||
}
|
||||
|
||||
fileSize := fileInfo.Size()
|
||||
@@ -50,10 +50,10 @@ func OpenDBFile(dbPath string, pageSize int) (*DBFile, error) {
|
||||
buffer := make([]byte, pageSize)
|
||||
n, err := io.ReadFull(fp, buffer)
|
||||
if err != nil {
|
||||
return nil, errors.DecryptReadFileFailed(dbPath, err)
|
||||
return nil, errors.ReadFileFailed(dbPath, err)
|
||||
}
|
||||
if n != pageSize {
|
||||
return nil, errors.DecryptIncompleteRead(fmt.Errorf("read %d bytes, expected %d", n, pageSize))
|
||||
return nil, errors.IncompleteRead(fmt.Errorf("read %d bytes, expected %d", n, pageSize))
|
||||
}
|
||||
|
||||
if bytes.Equal(buffer[:len(SQLiteHeader)-1], []byte(SQLiteHeader[:len(SQLiteHeader)-1])) {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/sjzar/chatlog/internal/errors"
|
||||
"github.com/sjzar/chatlog/internal/wechat/decrypt/common"
|
||||
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
@@ -75,7 +76,7 @@ func (d *V3Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string,
|
||||
// 解码密钥
|
||||
key, err := hex.DecodeString(hexKey)
|
||||
if err != nil {
|
||||
return errors.DecryptDecodeKeyFailed(err)
|
||||
return errors.DecodeKeyFailed(err)
|
||||
}
|
||||
|
||||
// 打开数据库文件并读取基本信息
|
||||
@@ -95,14 +96,14 @@ func (d *V3Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string,
|
||||
// 打开数据库文件
|
||||
dbFile, err := os.Open(dbfile)
|
||||
if err != nil {
|
||||
return errors.DecryptOpenFileFailed(dbfile, err)
|
||||
return errors.OpenFileFailed(dbfile, err)
|
||||
}
|
||||
defer dbFile.Close()
|
||||
|
||||
// 写入 SQLite 头
|
||||
_, err = output.Write([]byte(common.SQLiteHeader))
|
||||
if err != nil {
|
||||
return errors.DecryptWriteOutputFailed(err)
|
||||
return errors.WriteOutputFailed(err)
|
||||
}
|
||||
|
||||
// 处理每一页
|
||||
@@ -112,7 +113,7 @@ func (d *V3Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string,
|
||||
// 检查是否取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return errors.DecryptOperationCanceled()
|
||||
return errors.ErrDecryptOperationCanceled
|
||||
default:
|
||||
// 继续处理
|
||||
}
|
||||
@@ -126,7 +127,7 @@ func (d *V3Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string,
|
||||
break
|
||||
}
|
||||
}
|
||||
return errors.DecryptReadFileFailed(dbfile, err)
|
||||
return errors.ReadFileFailed(dbfile, err)
|
||||
}
|
||||
|
||||
// 检查页面是否全为零
|
||||
@@ -142,7 +143,7 @@ func (d *V3Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string,
|
||||
// 写入零页面
|
||||
_, err = output.Write(pageBuf)
|
||||
if err != nil {
|
||||
return errors.DecryptWriteOutputFailed(err)
|
||||
return errors.WriteOutputFailed(err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -156,7 +157,7 @@ func (d *V3Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string,
|
||||
// 写入解密后的页面
|
||||
_, err = output.Write(decryptedData)
|
||||
if err != nil {
|
||||
return errors.DecryptWriteOutputFailed(err)
|
||||
return errors.WriteOutputFailed(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -80,7 +80,7 @@ func (d *V4Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string,
|
||||
// 解码密钥
|
||||
key, err := hex.DecodeString(hexKey)
|
||||
if err != nil {
|
||||
return errors.DecryptDecodeKeyFailed(err)
|
||||
return errors.DecodeKeyFailed(err)
|
||||
}
|
||||
|
||||
// 打开数据库文件并读取基本信息
|
||||
@@ -100,14 +100,14 @@ func (d *V4Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string,
|
||||
// 打开数据库文件
|
||||
dbFile, err := os.Open(dbfile)
|
||||
if err != nil {
|
||||
return errors.DecryptOpenFileFailed(dbfile, err)
|
||||
return errors.OpenFileFailed(dbfile, err)
|
||||
}
|
||||
defer dbFile.Close()
|
||||
|
||||
// 写入SQLite头
|
||||
_, err = output.Write([]byte(common.SQLiteHeader))
|
||||
if err != nil {
|
||||
return errors.DecryptWriteOutputFailed(err)
|
||||
return errors.WriteOutputFailed(err)
|
||||
}
|
||||
|
||||
// 处理每一页
|
||||
@@ -117,7 +117,7 @@ func (d *V4Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string,
|
||||
// 检查是否取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return errors.DecryptOperationCanceled()
|
||||
return errors.ErrDecryptOperationCanceled
|
||||
default:
|
||||
// 继续处理
|
||||
}
|
||||
@@ -131,7 +131,7 @@ func (d *V4Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string,
|
||||
break
|
||||
}
|
||||
}
|
||||
return errors.DecryptReadFileFailed(dbfile, err)
|
||||
return errors.ReadFileFailed(dbfile, err)
|
||||
}
|
||||
|
||||
// 检查页面是否全为零
|
||||
@@ -147,7 +147,7 @@ func (d *V4Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string,
|
||||
// 写入零页面
|
||||
_, err = output.Write(pageBuf)
|
||||
if err != nil {
|
||||
return errors.DecryptWriteOutputFailed(err)
|
||||
return errors.WriteOutputFailed(err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -161,7 +161,7 @@ func (d *V4Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string,
|
||||
// 写入解密后的页面
|
||||
_, err = output.Write(decryptedData)
|
||||
if err != nil {
|
||||
return errors.DecryptWriteOutputFailed(err)
|
||||
return errors.WriteOutputFailed(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,6 @@ package decrypt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/errors"
|
||||
@@ -10,12 +9,6 @@ import (
|
||||
"github.com/sjzar/chatlog/internal/wechat/decrypt/windows"
|
||||
)
|
||||
|
||||
// 错误定义
|
||||
var (
|
||||
ErrInvalidVersion = fmt.Errorf("invalid version, must be 3 or 4")
|
||||
ErrUnsupportedPlatform = fmt.Errorf("unsupported platform")
|
||||
)
|
||||
|
||||
// Decryptor 定义数据库解密的接口
|
||||
type Decryptor interface {
|
||||
// Decrypt 解密数据库
|
||||
|
||||
@@ -78,7 +78,7 @@ func (d *V3Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string,
|
||||
// 解码密钥
|
||||
key, err := hex.DecodeString(hexKey)
|
||||
if err != nil {
|
||||
return errors.DecryptDecodeKeyFailed(err)
|
||||
return errors.DecodeKeyFailed(err)
|
||||
}
|
||||
|
||||
// 打开数据库文件并读取基本信息
|
||||
@@ -98,14 +98,14 @@ func (d *V3Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string,
|
||||
// 打开数据库文件
|
||||
dbFile, err := os.Open(dbfile)
|
||||
if err != nil {
|
||||
return errors.DecryptOpenFileFailed(dbfile, err)
|
||||
return errors.OpenFileFailed(dbfile, err)
|
||||
}
|
||||
defer dbFile.Close()
|
||||
|
||||
// 写入SQLite头
|
||||
_, err = output.Write([]byte(common.SQLiteHeader))
|
||||
if err != nil {
|
||||
return errors.DecryptWriteOutputFailed(err)
|
||||
return errors.WriteOutputFailed(err)
|
||||
}
|
||||
|
||||
// 处理每一页
|
||||
@@ -115,7 +115,7 @@ func (d *V3Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string,
|
||||
// 检查是否取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return errors.DecryptOperationCanceled()
|
||||
return errors.ErrDecryptOperationCanceled
|
||||
default:
|
||||
// 继续处理
|
||||
}
|
||||
@@ -129,7 +129,7 @@ func (d *V3Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string,
|
||||
break
|
||||
}
|
||||
}
|
||||
return errors.DecryptReadFileFailed(dbfile, err)
|
||||
return errors.ReadFileFailed(dbfile, err)
|
||||
}
|
||||
|
||||
// 检查页面是否全为零
|
||||
@@ -145,7 +145,7 @@ func (d *V3Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string,
|
||||
// 写入零页面
|
||||
_, err = output.Write(pageBuf)
|
||||
if err != nil {
|
||||
return errors.DecryptWriteOutputFailed(err)
|
||||
return errors.WriteOutputFailed(err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -159,7 +159,7 @@ func (d *V3Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string,
|
||||
// 写入解密后的页面
|
||||
_, err = output.Write(decryptedData)
|
||||
if err != nil {
|
||||
return errors.DecryptWriteOutputFailed(err)
|
||||
return errors.WriteOutputFailed(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
|
||||
"github.com/sjzar/chatlog/internal/errors"
|
||||
"github.com/sjzar/chatlog/internal/wechat/decrypt/common"
|
||||
|
||||
"golang.org/x/crypto/pbkdf2"
|
||||
)
|
||||
|
||||
@@ -76,7 +77,7 @@ func (d *V4Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string,
|
||||
// 解码密钥
|
||||
key, err := hex.DecodeString(hexKey)
|
||||
if err != nil {
|
||||
return errors.DecryptDecodeKeyFailed(err)
|
||||
return errors.DecodeKeyFailed(err)
|
||||
}
|
||||
|
||||
// 打开数据库文件并读取基本信息
|
||||
@@ -96,14 +97,14 @@ func (d *V4Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string,
|
||||
// 打开数据库文件
|
||||
dbFile, err := os.Open(dbfile)
|
||||
if err != nil {
|
||||
return errors.DecryptOpenFileFailed(dbfile, err)
|
||||
return errors.OpenFileFailed(dbfile, err)
|
||||
}
|
||||
defer dbFile.Close()
|
||||
|
||||
// 写入SQLite头
|
||||
_, err = output.Write([]byte(common.SQLiteHeader))
|
||||
if err != nil {
|
||||
return errors.DecryptWriteOutputFailed(err)
|
||||
return errors.WriteOutputFailed(err)
|
||||
}
|
||||
|
||||
// 处理每一页
|
||||
@@ -113,7 +114,7 @@ func (d *V4Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string,
|
||||
// 检查是否取消
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return errors.DecryptOperationCanceled()
|
||||
return errors.ErrDecryptOperationCanceled
|
||||
default:
|
||||
// 继续处理
|
||||
}
|
||||
@@ -127,7 +128,7 @@ func (d *V4Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string,
|
||||
break
|
||||
}
|
||||
}
|
||||
return errors.DecryptReadFileFailed(dbfile, err)
|
||||
return errors.ReadFileFailed(dbfile, err)
|
||||
}
|
||||
|
||||
// 检查页面是否全为零
|
||||
@@ -143,7 +144,7 @@ func (d *V4Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string,
|
||||
// 写入零页面
|
||||
_, err = output.Write(pageBuf)
|
||||
if err != nil {
|
||||
return errors.DecryptWriteOutputFailed(err)
|
||||
return errors.WriteOutputFailed(err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -157,7 +158,7 @@ func (d *V4Decryptor) Decrypt(ctx context.Context, dbfile string, hexKey string,
|
||||
// 写入解密后的页面
|
||||
_, err = output.Write(decryptedData)
|
||||
if err != nil {
|
||||
return errors.DecryptWriteOutputFailed(err)
|
||||
return errors.WriteOutputFailed(err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,9 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/sjzar/chatlog/internal/errors"
|
||||
)
|
||||
|
||||
// FIXME 按照 region 读取效率较低,512MB 内存读取耗时约 18s
|
||||
@@ -38,14 +41,14 @@ func (g *Glance) Read() ([]byte, error) {
|
||||
g.MemRegions = MemRegionsFilter(regions)
|
||||
|
||||
if len(g.MemRegions) == 0 {
|
||||
return nil, fmt.Errorf("no memory regions found")
|
||||
return nil, errors.ErrNoMemoryRegionsFound
|
||||
}
|
||||
|
||||
region := g.MemRegions[0]
|
||||
|
||||
// 1. Create pipe file
|
||||
if err := exec.Command("mkfifo", g.pipePath).Run(); err != nil {
|
||||
return nil, fmt.Errorf("failed to create pipe file: %w", err)
|
||||
return nil, errors.CreatePipeFileFailed(err)
|
||||
}
|
||||
defer os.Remove(g.pipePath)
|
||||
|
||||
@@ -56,7 +59,7 @@ func (g *Glance) Read() ([]byte, error) {
|
||||
// Open pipe for reading
|
||||
file, err := os.OpenFile(g.pipePath, os.O_RDONLY, 0600)
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("failed to open pipe for reading: %w", err)
|
||||
errCh <- errors.OpenPipeFileFailed(err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
@@ -64,7 +67,7 @@ func (g *Glance) Read() ([]byte, error) {
|
||||
// Read all data from pipe
|
||||
data, err := io.ReadAll(file)
|
||||
if err != nil {
|
||||
errCh <- fmt.Errorf("failed to read from pipe: %w", err)
|
||||
errCh <- errors.ReadPipeFileFailed(err)
|
||||
return
|
||||
}
|
||||
dataCh <- data
|
||||
@@ -80,12 +83,12 @@ func (g *Glance) Read() ([]byte, error) {
|
||||
// Set up stdout pipe for monitoring (optional)
|
||||
stdout, err := cmd.StdoutPipe()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create stdout pipe: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Start the command
|
||||
if err := cmd.Start(); err != nil {
|
||||
return nil, fmt.Errorf("failed to start lldb: %w", err)
|
||||
return nil, errors.RunCmdFailed(err)
|
||||
}
|
||||
|
||||
// Monitor lldb output (optional)
|
||||
@@ -102,16 +105,16 @@ func (g *Glance) Read() ([]byte, error) {
|
||||
case data := <-dataCh:
|
||||
g.data = data
|
||||
case err := <-errCh:
|
||||
return nil, fmt.Errorf("failed to read memory: %w", err)
|
||||
return nil, errors.ReadMemoryFailed(err)
|
||||
case <-time.After(30 * time.Second):
|
||||
cmd.Process.Kill()
|
||||
return nil, fmt.Errorf("timeout waiting for memory data")
|
||||
return nil, errors.ErrReadMemoryTimeout
|
||||
}
|
||||
|
||||
// Wait for the command to finish
|
||||
if err := cmd.Wait(); err != nil {
|
||||
// We already have the data, so just log the error
|
||||
fmt.Printf("Warning: lldb process exited with error: %v\n", err)
|
||||
log.Err(err).Msg("lldb process exited with error")
|
||||
}
|
||||
|
||||
return g.data, nil
|
||||
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/errors"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -31,7 +33,7 @@ func GetVmmap(pid uint32) ([]MemRegion, error) {
|
||||
cmd := exec.Command(CommandVmmap, "-wide", fmt.Sprintf("%d", pid))
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error executing vmmap command: %w", err)
|
||||
return nil, errors.RunCmdFailed(err)
|
||||
}
|
||||
|
||||
// Parse the output using the existing LoadVmmap function
|
||||
|
||||
@@ -4,12 +4,12 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/errors"
|
||||
"github.com/sjzar/chatlog/internal/wechat/decrypt"
|
||||
"github.com/sjzar/chatlog/internal/wechat/key/darwin/glance"
|
||||
"github.com/sjzar/chatlog/internal/wechat/model"
|
||||
@@ -29,16 +29,16 @@ func NewV3Extractor() *V3Extractor {
|
||||
|
||||
func (e *V3Extractor) Extract(ctx context.Context, proc *model.Process) (string, error) {
|
||||
if proc.Status == model.StatusOffline {
|
||||
return "", fmt.Errorf("WeChat is offline")
|
||||
return "", errors.ErrWeChatOffline
|
||||
}
|
||||
|
||||
// Check if SIP is disabled, as it's required for memory reading on macOS
|
||||
if !glance.IsSIPDisabled() {
|
||||
return "", fmt.Errorf("System Integrity Protection (SIP) is enabled, cannot read process memory")
|
||||
return "", errors.ErrSIPEnabled
|
||||
}
|
||||
|
||||
if e.validator == nil {
|
||||
return "", fmt.Errorf("validator not set")
|
||||
return "", errors.ErrValidatorNotSet
|
||||
}
|
||||
|
||||
// Create context to control all goroutines
|
||||
@@ -57,7 +57,7 @@ func (e *V3Extractor) Extract(ctx context.Context, proc *model.Process) (string,
|
||||
if workerCount > MaxWorkersV3 {
|
||||
workerCount = MaxWorkersV3
|
||||
}
|
||||
logrus.Debug("Starting ", workerCount, " workers for V3 key search")
|
||||
log.Debug().Msgf("Starting %d workers for V3 key search", workerCount)
|
||||
|
||||
// Start consumer goroutines
|
||||
var workerWaitGroup sync.WaitGroup
|
||||
@@ -77,7 +77,7 @@ func (e *V3Extractor) Extract(ctx context.Context, proc *model.Process) (string,
|
||||
defer close(memoryChannel) // Close channel when producer is done
|
||||
err := e.findMemory(searchCtx, uint32(proc.PID), memoryChannel)
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
log.Err(err).Msg("Failed to read memory")
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -98,7 +98,7 @@ func (e *V3Extractor) Extract(ctx context.Context, proc *model.Process) (string,
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no valid key found")
|
||||
return "", errors.ErrNoValidKey
|
||||
}
|
||||
|
||||
// findMemory searches for memory regions using Glance
|
||||
@@ -109,15 +109,15 @@ func (e *V3Extractor) findMemory(ctx context.Context, pid uint32, memoryChannel
|
||||
// Read memory data
|
||||
memory, err := g.Read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read process memory: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Debug("Read memory region, size: ", len(memory), " bytes")
|
||||
log.Debug().Msgf("Read memory region, size: %d bytes", len(memory))
|
||||
|
||||
// Send memory data to channel for processing
|
||||
select {
|
||||
case memoryChannel <- memory:
|
||||
logrus.Debug("Sent memory region for analysis")
|
||||
log.Debug().Msg("Memory region sent for analysis")
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
@@ -146,7 +146,7 @@ func (e *V3Extractor) worker(ctx context.Context, memoryChannel <-chan []byte, r
|
||||
default:
|
||||
}
|
||||
|
||||
logrus.Debugf("Searching for V3 key in memory region, size: %d bytes", len(memory))
|
||||
log.Debug().Msgf("Searching for V3 key in memory region, size: %d bytes", len(memory))
|
||||
|
||||
// Find pattern from end to beginning
|
||||
index = bytes.LastIndex(memory[:index], keyPattern)
|
||||
@@ -154,7 +154,7 @@ func (e *V3Extractor) worker(ctx context.Context, memoryChannel <-chan []byte, r
|
||||
break // No more matches found
|
||||
}
|
||||
|
||||
logrus.Debugf("Found potential V3 key pattern in memory region, index: %d", index)
|
||||
log.Debug().Msgf("Found potential V3 key pattern in memory region, index: %d", index)
|
||||
|
||||
// For V3, the key is 32 bytes and starts right after the pattern
|
||||
if index+24+32 > len(memory) {
|
||||
@@ -170,7 +170,7 @@ func (e *V3Extractor) worker(ctx context.Context, memoryChannel <-chan []byte, r
|
||||
if e.validator.Validate(keyData) {
|
||||
select {
|
||||
case resultChannel <- hex.EncodeToString(keyData):
|
||||
logrus.Debug("Valid key found for V3 database")
|
||||
log.Debug().Msg("Key found: " + hex.EncodeToString(keyData))
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
@@ -4,12 +4,12 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/errors"
|
||||
"github.com/sjzar/chatlog/internal/wechat/decrypt"
|
||||
"github.com/sjzar/chatlog/internal/wechat/key/darwin/glance"
|
||||
"github.com/sjzar/chatlog/internal/wechat/model"
|
||||
@@ -29,16 +29,16 @@ func NewV4Extractor() *V4Extractor {
|
||||
|
||||
func (e *V4Extractor) Extract(ctx context.Context, proc *model.Process) (string, error) {
|
||||
if proc.Status == model.StatusOffline {
|
||||
return "", fmt.Errorf("WeChat is offline")
|
||||
return "", errors.ErrWeChatOffline
|
||||
}
|
||||
|
||||
// Check if SIP is disabled, as it's required for memory reading on macOS
|
||||
if !glance.IsSIPDisabled() {
|
||||
return "", fmt.Errorf("System Integrity Protection (SIP) is enabled, cannot read process memory")
|
||||
return "", errors.ErrSIPEnabled
|
||||
}
|
||||
|
||||
if e.validator == nil {
|
||||
return "", fmt.Errorf("validator not set")
|
||||
return "", errors.ErrValidatorNotSet
|
||||
}
|
||||
|
||||
// Create context to control all goroutines
|
||||
@@ -57,7 +57,7 @@ func (e *V4Extractor) Extract(ctx context.Context, proc *model.Process) (string,
|
||||
if workerCount > MaxWorkers {
|
||||
workerCount = MaxWorkers
|
||||
}
|
||||
logrus.Debug("Starting ", workerCount, " workers for V4 key search")
|
||||
log.Debug().Msgf("Starting %d workers for V4 key search", workerCount)
|
||||
|
||||
// Start consumer goroutines
|
||||
var workerWaitGroup sync.WaitGroup
|
||||
@@ -77,7 +77,7 @@ func (e *V4Extractor) Extract(ctx context.Context, proc *model.Process) (string,
|
||||
defer close(memoryChannel) // Close channel when producer is done
|
||||
err := e.findMemory(searchCtx, uint32(proc.PID), memoryChannel)
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
log.Err(err).Msg("Failed to read memory")
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -98,7 +98,7 @@ func (e *V4Extractor) Extract(ctx context.Context, proc *model.Process) (string,
|
||||
}
|
||||
}
|
||||
|
||||
return "", fmt.Errorf("no valid key found")
|
||||
return "", errors.ErrNoValidKey
|
||||
}
|
||||
|
||||
// findMemory searches for memory regions using Glance
|
||||
@@ -109,15 +109,15 @@ func (e *V4Extractor) findMemory(ctx context.Context, pid uint32, memoryChannel
|
||||
// Read memory data
|
||||
memory, err := g.Read()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read process memory: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
logrus.Debug("Read memory region, size: ", len(memory), " bytes")
|
||||
log.Debug().Msgf("Read memory region, size: %d bytes", len(memory))
|
||||
|
||||
// Send memory data to channel for processing
|
||||
select {
|
||||
case memoryChannel <- memory:
|
||||
logrus.Debug("Sent memory region for analysis")
|
||||
log.Debug().Msg("Memory region sent for analysis")
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
@@ -167,7 +167,7 @@ func (e *V4Extractor) worker(ctx context.Context, memoryChannel <-chan []byte, r
|
||||
if e.validator.Validate(keyData) {
|
||||
select {
|
||||
case resultChannel <- hex.EncodeToString(keyData):
|
||||
logrus.Debug("Valid key found for V4 database")
|
||||
log.Debug().Msg("Key found: " + hex.EncodeToString(keyData))
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
@@ -2,20 +2,14 @@ package key
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/errors"
|
||||
"github.com/sjzar/chatlog/internal/wechat/decrypt"
|
||||
"github.com/sjzar/chatlog/internal/wechat/key/darwin"
|
||||
"github.com/sjzar/chatlog/internal/wechat/key/windows"
|
||||
"github.com/sjzar/chatlog/internal/wechat/model"
|
||||
)
|
||||
|
||||
// 错误定义
|
||||
var (
|
||||
ErrInvalidVersion = fmt.Errorf("invalid version, must be 3 or 4")
|
||||
ErrUnsupportedPlatform = fmt.Errorf("unsupported platform")
|
||||
)
|
||||
|
||||
// Extractor 定义密钥提取器接口
|
||||
type Extractor interface {
|
||||
// Extract 从进程中提取密钥
|
||||
@@ -36,6 +30,6 @@ func NewExtractor(platform string, version int) (Extractor, error) {
|
||||
case platform == "darwin" && version == 4:
|
||||
return darwin.NewV4Extractor(), nil
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: %s v%d", ErrUnsupportedPlatform, platform, version)
|
||||
return nil, errors.PlatformUnsupported(platform, version)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
package windows
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/wechat/decrypt"
|
||||
)
|
||||
|
||||
// Common error definitions
|
||||
var (
|
||||
ErrWeChatOffline = errors.New("wechat is not logged in")
|
||||
ErrOpenProcess = errors.New("failed to open process")
|
||||
ErrCheckProcessBits = errors.New("failed to check process architecture")
|
||||
ErrFindWeChatDLL = errors.New("WeChatWin.dll module not found")
|
||||
ErrNoValidKey = errors.New("no valid key found")
|
||||
)
|
||||
|
||||
type V3Extractor struct {
|
||||
validator *decrypt.Validator
|
||||
}
|
||||
|
||||
@@ -10,9 +10,10 @@ import (
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/sys/windows"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/errors"
|
||||
"github.com/sjzar/chatlog/internal/wechat/model"
|
||||
"github.com/sjzar/chatlog/pkg/util"
|
||||
)
|
||||
@@ -24,20 +25,20 @@ const (
|
||||
|
||||
func (e *V3Extractor) Extract(ctx context.Context, proc *model.Process) (string, error) {
|
||||
if proc.Status == model.StatusOffline {
|
||||
return "", ErrWeChatOffline
|
||||
return "", errors.ErrWeChatOffline
|
||||
}
|
||||
|
||||
// Open WeChat process
|
||||
handle, err := windows.OpenProcess(windows.PROCESS_QUERY_INFORMATION|windows.PROCESS_VM_READ, false, proc.PID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: %v", ErrOpenProcess, err)
|
||||
return "", errors.OpenProcessFailed(err)
|
||||
}
|
||||
defer windows.CloseHandle(handle)
|
||||
|
||||
// Check process architecture
|
||||
is64Bit, err := util.Is64Bit(handle)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: %v", ErrCheckProcessBits, err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Create context to control all goroutines
|
||||
@@ -56,7 +57,7 @@ func (e *V3Extractor) Extract(ctx context.Context, proc *model.Process) (string,
|
||||
if workerCount > MaxWorkers {
|
||||
workerCount = MaxWorkers
|
||||
}
|
||||
logrus.Debug("Starting ", workerCount, " workers for V3 key search")
|
||||
log.Debug().Msgf("Starting %d workers for V3 key search", workerCount)
|
||||
|
||||
// Start consumer goroutines
|
||||
var workerWaitGroup sync.WaitGroup
|
||||
@@ -76,7 +77,7 @@ func (e *V3Extractor) Extract(ctx context.Context, proc *model.Process) (string,
|
||||
defer close(memoryChannel) // Close channel when producer is done
|
||||
err := e.findMemory(searchCtx, handle, proc.PID, memoryChannel)
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
log.Err(err).Msg("Failed to find memory regions")
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -97,7 +98,7 @@ func (e *V3Extractor) Extract(ctx context.Context, proc *model.Process) (string,
|
||||
}
|
||||
}
|
||||
|
||||
return "", ErrNoValidKey
|
||||
return "", errors.ErrNoValidKey
|
||||
}
|
||||
|
||||
// findMemoryV3 searches for writable memory regions in WeChatWin.dll for V3 version
|
||||
@@ -105,9 +106,9 @@ func (e *V3Extractor) findMemory(ctx context.Context, handle windows.Handle, pid
|
||||
// Find WeChatWin.dll module
|
||||
module, isFound := FindModule(pid, V3ModuleName)
|
||||
if !isFound {
|
||||
return ErrFindWeChatDLL
|
||||
return errors.ErrWeChatDLLNotFound
|
||||
}
|
||||
logrus.Debug("Found WeChatWin.dll module at base address: 0x", fmt.Sprintf("%X", module.ModBaseAddr))
|
||||
log.Debug().Msg("Found WeChatWin.dll module at base address: 0x" + fmt.Sprintf("%X", module.ModBaseAddr))
|
||||
|
||||
// Read writable memory regions
|
||||
baseAddr := uintptr(module.ModBaseAddr)
|
||||
@@ -141,7 +142,7 @@ func (e *V3Extractor) findMemory(ctx context.Context, handle windows.Handle, pid
|
||||
if err = windows.ReadProcessMemory(handle, currentAddr, &memory[0], regionSize, nil); err == nil {
|
||||
select {
|
||||
case memoryChannel <- memory:
|
||||
logrus.Debug("Sent memory region for analysis, size: ", regionSize, " bytes")
|
||||
log.Debug().Msgf("Memory region: 0x%X - 0x%X, size: %d bytes", currentAddr, currentAddr+regionSize, regionSize)
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
@@ -198,7 +199,7 @@ func (e *V3Extractor) worker(ctx context.Context, handle windows.Handle, is64Bit
|
||||
if key := e.validateKey(handle, ptrValue); key != "" {
|
||||
select {
|
||||
case resultChannel <- key:
|
||||
logrus.Debug("Valid key found for V3 database")
|
||||
log.Debug().Msg("Valid key found: " + key)
|
||||
return
|
||||
default:
|
||||
}
|
||||
@@ -230,7 +231,7 @@ func FindModule(pid uint32, name string) (module windows.ModuleEntry32, isFound
|
||||
// Create module snapshot
|
||||
snapshot, err := windows.CreateToolhelp32Snapshot(windows.TH32CS_SNAPMODULE|windows.TH32CS_SNAPMODULE32, pid)
|
||||
if err != nil {
|
||||
logrus.Debug("Failed to create module snapshot: ", err)
|
||||
log.Debug().Msgf("Failed to create module snapshot for PID %d: %v", pid, err)
|
||||
return module, false
|
||||
}
|
||||
defer windows.CloseHandle(snapshot)
|
||||
@@ -240,7 +241,7 @@ func FindModule(pid uint32, name string) (module windows.ModuleEntry32, isFound
|
||||
|
||||
// Get the first module
|
||||
if err := windows.Module32First(snapshot, &module); err != nil {
|
||||
logrus.Debug("Failed to get first module: ", err)
|
||||
log.Debug().Msgf("Module32First failed for PID %d: %v", pid, err)
|
||||
return module, false
|
||||
}
|
||||
|
||||
|
||||
@@ -5,14 +5,14 @@ import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"runtime"
|
||||
"sync"
|
||||
"unsafe"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
"github.com/rs/zerolog/log"
|
||||
"golang.org/x/sys/windows"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/errors"
|
||||
"github.com/sjzar/chatlog/internal/wechat/model"
|
||||
)
|
||||
|
||||
@@ -22,13 +22,13 @@ const (
|
||||
|
||||
func (e *V4Extractor) Extract(ctx context.Context, proc *model.Process) (string, error) {
|
||||
if proc.Status == model.StatusOffline {
|
||||
return "", ErrWeChatOffline
|
||||
return "", errors.ErrWeChatOffline
|
||||
}
|
||||
|
||||
// Open process handle
|
||||
handle, err := windows.OpenProcess(windows.PROCESS_VM_READ|windows.PROCESS_QUERY_INFORMATION, false, proc.PID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: %v", ErrOpenProcess, err)
|
||||
return "", errors.OpenProcessFailed(err)
|
||||
}
|
||||
defer windows.CloseHandle(handle)
|
||||
|
||||
@@ -48,7 +48,7 @@ func (e *V4Extractor) Extract(ctx context.Context, proc *model.Process) (string,
|
||||
if workerCount > MaxWorkers {
|
||||
workerCount = MaxWorkers
|
||||
}
|
||||
logrus.Debug("Starting ", workerCount, " workers for V4 key search")
|
||||
log.Debug().Msgf("Starting %d workers for V4 key search", workerCount)
|
||||
|
||||
// Start consumer goroutines
|
||||
var workerWaitGroup sync.WaitGroup
|
||||
@@ -68,7 +68,7 @@ func (e *V4Extractor) Extract(ctx context.Context, proc *model.Process) (string,
|
||||
defer close(memoryChannel) // Close channel when producer is done
|
||||
err := e.findMemory(searchCtx, handle, memoryChannel)
|
||||
if err != nil {
|
||||
logrus.Error(err)
|
||||
log.Err(err).Msg("Failed to find memory regions")
|
||||
}
|
||||
}()
|
||||
|
||||
@@ -89,7 +89,7 @@ func (e *V4Extractor) Extract(ctx context.Context, proc *model.Process) (string,
|
||||
}
|
||||
}
|
||||
|
||||
return "", ErrNoValidKey
|
||||
return "", errors.ErrNoValidKey
|
||||
}
|
||||
|
||||
// findMemoryV4 searches for writable memory regions for V4 version
|
||||
@@ -101,7 +101,7 @@ func (e *V4Extractor) findMemory(ctx context.Context, handle windows.Handle, mem
|
||||
if runtime.GOARCH == "amd64" {
|
||||
maxAddr = uintptr(0x7FFFFFFFFFFF) // 64-bit process space limit
|
||||
}
|
||||
logrus.Debug("Scanning memory regions from 0x", fmt.Sprintf("%X", minAddr), " to 0x", fmt.Sprintf("%X", maxAddr))
|
||||
log.Debug().Msgf("Scanning memory regions from 0x%X to 0x%X", minAddr, maxAddr)
|
||||
|
||||
currentAddr := minAddr
|
||||
|
||||
@@ -131,7 +131,7 @@ func (e *V4Extractor) findMemory(ctx context.Context, handle windows.Handle, mem
|
||||
if err = windows.ReadProcessMemory(handle, currentAddr, &memory[0], regionSize, nil); err == nil {
|
||||
select {
|
||||
case memoryChannel <- memory:
|
||||
logrus.Debug("Sent memory region for analysis, size: ", regionSize, " bytes")
|
||||
log.Debug().Msgf("Memory region for analysis: 0x%X - 0x%X, size: %d bytes", currentAddr, currentAddr+regionSize, regionSize)
|
||||
case <-ctx.Done():
|
||||
return nil
|
||||
}
|
||||
@@ -185,7 +185,7 @@ func (e *V4Extractor) worker(ctx context.Context, handle windows.Handle, memoryC
|
||||
if key := e.validateKey(handle, ptrValue); key != "" {
|
||||
select {
|
||||
case resultChannel <- key:
|
||||
logrus.Debug("Valid key found for V4 database")
|
||||
log.Debug().Msg("Valid key found: " + key)
|
||||
return
|
||||
default:
|
||||
}
|
||||
|
||||
@@ -2,9 +2,9 @@ package wechat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"runtime"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/errors"
|
||||
"github.com/sjzar/chatlog/internal/wechat/model"
|
||||
"github.com/sjzar/chatlog/internal/wechat/process"
|
||||
)
|
||||
@@ -87,7 +87,7 @@ func (m *Manager) GetAccount(name string) (*Account, error) {
|
||||
func (m *Manager) GetProcess(name string) (*model.Process, error) {
|
||||
p, ok := m.processMap[name]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("account not found: %s", name)
|
||||
return nil, errors.WeChatAccountNotFound(name)
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
package darwin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/shirou/gopsutil/v4/process"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/errors"
|
||||
"github.com/sjzar/chatlog/internal/wechat/model"
|
||||
"github.com/sjzar/chatlog/pkg/appver"
|
||||
)
|
||||
@@ -33,7 +33,7 @@ func NewDetector() *Detector {
|
||||
func (d *Detector) FindProcesses() ([]*model.Process, error) {
|
||||
processes, err := process.Processes()
|
||||
if err != nil {
|
||||
log.Errorf("获取进程列表失败: %v", err)
|
||||
log.Err(err).Msg("获取进程列表失败")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -47,7 +47,7 @@ func (d *Detector) FindProcesses() ([]*model.Process, error) {
|
||||
// 获取进程信息
|
||||
procInfo, err := d.getProcessInfo(p)
|
||||
if err != nil {
|
||||
log.Errorf("获取进程 %d 的信息失败: %v", p.Pid, err)
|
||||
log.Err(err).Msgf("获取进程 %d 的信息失败", p.Pid)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -68,7 +68,7 @@ func (d *Detector) getProcessInfo(p *process.Process) (*model.Process, error) {
|
||||
// 获取可执行文件路径
|
||||
exePath, err := p.Exe()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
log.Err(err).Msg("获取可执行文件路径失败")
|
||||
return nil, err
|
||||
}
|
||||
procInfo.ExePath = exePath
|
||||
@@ -77,7 +77,7 @@ func (d *Detector) getProcessInfo(p *process.Process) (*model.Process, error) {
|
||||
// 注意:macOS 的版本获取方式可能与 Windows 不同
|
||||
versionInfo, err := appver.New(exePath)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
log.Err(err).Msg("获取版本信息失败")
|
||||
procInfo.Version = 3
|
||||
procInfo.FullVersion = "3.0.0"
|
||||
} else {
|
||||
@@ -87,7 +87,7 @@ func (d *Detector) getProcessInfo(p *process.Process) (*model.Process, error) {
|
||||
|
||||
// 初始化附加信息(数据目录、账户名)
|
||||
if err := d.initializeProcessInfo(p, procInfo); err != nil {
|
||||
log.Errorf("初始化进程信息失败: %v", err)
|
||||
log.Err(err).Msg("初始化进程信息失败")
|
||||
// 即使初始化失败也返回部分信息
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ func (d *Detector) initializeProcessInfo(p *process.Process, info *model.Process
|
||||
// 使用 lsof 命令获取进程打开的文件
|
||||
files, err := d.getOpenFiles(int(p.Pid))
|
||||
if err != nil {
|
||||
log.Error("获取打开文件列表失败: ", err)
|
||||
log.Err(err).Msg("获取打开的文件失败")
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -112,7 +112,7 @@ func (d *Detector) initializeProcessInfo(p *process.Process, info *model.Process
|
||||
if strings.Contains(filePath, dbPath) {
|
||||
parts := strings.Split(filePath, string(filepath.Separator))
|
||||
if len(parts) < 4 {
|
||||
log.Debug("无效的文件路径格式: " + filePath)
|
||||
log.Debug().Msg("无效的文件路径格式: " + filePath)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -142,7 +142,7 @@ func (d *Detector) getOpenFiles(pid int) ([]string, error) {
|
||||
cmd := exec.Command("lsof", "-p", strconv.Itoa(pid), "-F", "n")
|
||||
output, err := cmd.Output()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("执行 lsof 命令失败: %v", err)
|
||||
return nil, errors.RunCmdFailed(err)
|
||||
}
|
||||
|
||||
// 解析 lsof -F n 输出
|
||||
|
||||
@@ -3,8 +3,8 @@ package windows
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/shirou/gopsutil/v4/process"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/wechat/model"
|
||||
"github.com/sjzar/chatlog/pkg/appver"
|
||||
@@ -29,7 +29,7 @@ func NewDetector() *Detector {
|
||||
func (d *Detector) FindProcesses() ([]*model.Process, error) {
|
||||
processes, err := process.Processes()
|
||||
if err != nil {
|
||||
log.Errorf("获取进程列表失败: %v", err)
|
||||
log.Err(err).Msg("获取进程列表失败")
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ func (d *Detector) FindProcesses() ([]*model.Process, error) {
|
||||
if name == V4ProcessName {
|
||||
cmdline, err := p.Cmdline()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
log.Err(err).Msg("获取进程命令行失败")
|
||||
continue
|
||||
}
|
||||
if strings.Contains(cmdline, "--") {
|
||||
@@ -56,7 +56,7 @@ func (d *Detector) FindProcesses() ([]*model.Process, error) {
|
||||
// 获取进程信息
|
||||
procInfo, err := d.getProcessInfo(p)
|
||||
if err != nil {
|
||||
log.Errorf("获取进程 %d 的信息失败: %v", p.Pid, err)
|
||||
log.Err(err).Msgf("获取进程 %d 的信息失败", p.Pid)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ func (d *Detector) getProcessInfo(p *process.Process) (*model.Process, error) {
|
||||
// 获取可执行文件路径
|
||||
exePath, err := p.Exe()
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
log.Err(err).Msg("获取可执行文件路径失败")
|
||||
return nil, err
|
||||
}
|
||||
procInfo.ExePath = exePath
|
||||
@@ -85,7 +85,7 @@ func (d *Detector) getProcessInfo(p *process.Process) (*model.Process, error) {
|
||||
// 获取版本信息
|
||||
versionInfo, err := appver.New(exePath)
|
||||
if err != nil {
|
||||
log.Error(err)
|
||||
log.Err(err).Msg("获取版本信息失败")
|
||||
return nil, err
|
||||
}
|
||||
procInfo.Version = versionInfo.Version
|
||||
@@ -93,7 +93,7 @@ func (d *Detector) getProcessInfo(p *process.Process) (*model.Process, error) {
|
||||
|
||||
// 初始化附加信息(数据目录、账户名)
|
||||
if err := initializeProcessInfo(p, procInfo); err != nil {
|
||||
log.Errorf("初始化进程信息失败: %v", err)
|
||||
log.Err(err).Msg("初始化进程信息失败")
|
||||
// 即使初始化失败也返回部分信息
|
||||
}
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/shirou/gopsutil/v4/process"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/wechat/model"
|
||||
)
|
||||
@@ -14,7 +14,7 @@ import (
|
||||
func initializeProcessInfo(p *process.Process, info *model.Process) error {
|
||||
files, err := p.OpenFiles()
|
||||
if err != nil {
|
||||
log.Error("获取打开文件列表失败: ", err)
|
||||
log.Err(err).Msgf("获取进程 %d 的打开文件失败", p.Pid)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -28,7 +28,7 @@ func initializeProcessInfo(p *process.Process, info *model.Process) error {
|
||||
filePath := f.Path[4:] // 移除 "\\?\" 前缀
|
||||
parts := strings.Split(filePath, string(filepath.Separator))
|
||||
if len(parts) < 4 {
|
||||
log.Debug("无效的文件路径格式: " + filePath)
|
||||
log.Debug().Msg("无效的文件路径: " + filePath)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -2,9 +2,9 @@ package wechat
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/errors"
|
||||
"github.com/sjzar/chatlog/internal/wechat/decrypt"
|
||||
"github.com/sjzar/chatlog/internal/wechat/key"
|
||||
"github.com/sjzar/chatlog/internal/wechat/model"
|
||||
@@ -71,28 +71,28 @@ func (a *Account) GetKey(ctx context.Context) (string, error) {
|
||||
|
||||
// 刷新进程状态
|
||||
if err := a.RefreshStatus(); err != nil {
|
||||
return "", fmt.Errorf("failed to refresh process status: %w", err)
|
||||
return "", errors.RefreshProcessStatusFailed(err)
|
||||
}
|
||||
|
||||
// 检查账号状态
|
||||
if a.Status != model.StatusOnline {
|
||||
return "", fmt.Errorf("account %s is not online", a.Name)
|
||||
return "", errors.WeChatAccountNotOnline(a.Name)
|
||||
}
|
||||
|
||||
// 创建密钥提取器 - 使用新的接口,传入平台和版本信息
|
||||
extractor, err := key.NewExtractor(a.Platform, a.Version)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create key extractor: %w", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
process, err := GetProcess(a.Name)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get process: %w", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
validator, err := decrypt.NewValidator(process.DataDir, process.Platform, process.Version)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to create validator: %w", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
extractor.SetValidate(validator)
|
||||
|
||||
@@ -6,14 +6,15 @@ import (
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/errors"
|
||||
"github.com/sjzar/chatlog/internal/model"
|
||||
"github.com/sjzar/chatlog/pkg/util"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -21,6 +22,7 @@ const (
|
||||
ContactFilePattern = "^wccontact_new2\\.db$"
|
||||
ChatRoomFilePattern = "^group_new\\.db$"
|
||||
SessionFilePattern = "^session_new\\.db$"
|
||||
MediaFilePattern = "^hldata\\.db$"
|
||||
)
|
||||
|
||||
type DataSource struct {
|
||||
@@ -29,6 +31,7 @@ type DataSource struct {
|
||||
contactDb *sql.DB
|
||||
chatRoomDb *sql.DB
|
||||
sessionDb *sql.DB
|
||||
mediaDb *sql.DB
|
||||
|
||||
talkerDBMap map[string]*sql.DB
|
||||
user2DisplayName map[string]string
|
||||
@@ -43,16 +46,19 @@ func New(path string) (*DataSource, error) {
|
||||
}
|
||||
|
||||
if err := ds.initMessageDbs(path); err != nil {
|
||||
return nil, fmt.Errorf("初始化消息数据库失败: %w", err)
|
||||
return nil, errors.DBInitFailed(err)
|
||||
}
|
||||
if err := ds.initContactDb(path); err != nil {
|
||||
return nil, fmt.Errorf("初始化联系人数据库失败: %w", err)
|
||||
return nil, errors.DBInitFailed(err)
|
||||
}
|
||||
if err := ds.initChatRoomDb(path); err != nil {
|
||||
return nil, fmt.Errorf("初始化群聊数据库失败: %w", err)
|
||||
return nil, errors.DBInitFailed(err)
|
||||
}
|
||||
if err := ds.initSessionDb(path); err != nil {
|
||||
return nil, fmt.Errorf("初始化会话数据库失败: %w", err)
|
||||
return nil, errors.DBInitFailed(err)
|
||||
}
|
||||
if err := ds.initMediaDb(path); err != nil {
|
||||
return nil, errors.DBInitFailed(err)
|
||||
}
|
||||
|
||||
return ds, nil
|
||||
@@ -62,11 +68,11 @@ func (ds *DataSource) initMessageDbs(path string) error {
|
||||
|
||||
files, err := util.FindFilesWithPatterns(path, MessageFilePattern, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查找消息数据库文件失败: %w", err)
|
||||
return errors.DBFileNotFound(path, MessageFilePattern, err)
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return fmt.Errorf("未找到任何消息数据库文件: %s", path)
|
||||
return errors.DBFileNotFound(path, MessageFilePattern, nil)
|
||||
}
|
||||
|
||||
// 处理每个数据库文件
|
||||
@@ -74,7 +80,7 @@ func (ds *DataSource) initMessageDbs(path string) error {
|
||||
// 连接数据库
|
||||
db, err := sql.Open("sqlite3", filePath)
|
||||
if err != nil {
|
||||
log.Printf("警告: 连接数据库 %s 失败: %v", filePath, err)
|
||||
log.Err(err).Msgf("连接数据库 %s 失败", filePath)
|
||||
continue
|
||||
}
|
||||
ds.messageDbs = append(ds.messageDbs, db)
|
||||
@@ -82,14 +88,14 @@ func (ds *DataSource) initMessageDbs(path string) error {
|
||||
// 获取所有表名
|
||||
rows, err := db.Query("SELECT name FROM sqlite_master WHERE type='table' AND name LIKE 'Chat_%'")
|
||||
if err != nil {
|
||||
log.Printf("警告: 获取表名失败: %v", err)
|
||||
log.Err(err).Msgf("数据库 %s 中没有 Chat 表", filePath)
|
||||
continue
|
||||
}
|
||||
|
||||
for rows.Next() {
|
||||
var tableName string
|
||||
if err := rows.Scan(&tableName); err != nil {
|
||||
log.Printf("警告: 扫描表名失败: %v", err)
|
||||
log.Err(err).Msgf("数据库 %s 扫描表名失败", filePath)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -110,16 +116,16 @@ func (ds *DataSource) initContactDb(path string) error {
|
||||
|
||||
files, err := util.FindFilesWithPatterns(path, ContactFilePattern, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查找联系人数据库文件失败: %w", err)
|
||||
return errors.DBFileNotFound(path, ContactFilePattern, err)
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return fmt.Errorf("未找到联系人数据库文件: %s", path)
|
||||
return errors.DBFileNotFound(path, ContactFilePattern, nil)
|
||||
}
|
||||
|
||||
ds.contactDb, err = sql.Open("sqlite3", files[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("连接联系人数据库失败: %w", err)
|
||||
return errors.DBConnectFailed(files[0], err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -128,19 +134,19 @@ func (ds *DataSource) initContactDb(path string) error {
|
||||
func (ds *DataSource) initChatRoomDb(path string) error {
|
||||
files, err := util.FindFilesWithPatterns(path, ChatRoomFilePattern, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查找群聊数据库文件失败: %w", err)
|
||||
return errors.DBFileNotFound(path, ChatRoomFilePattern, err)
|
||||
}
|
||||
if len(files) == 0 {
|
||||
return fmt.Errorf("未找到群聊数据库文件: %s", path)
|
||||
return errors.DBFileNotFound(path, ChatRoomFilePattern, nil)
|
||||
}
|
||||
ds.chatRoomDb, err = sql.Open("sqlite3", files[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("连接群聊数据库失败: %w", err)
|
||||
return errors.DBConnectFailed(files[0], err)
|
||||
}
|
||||
|
||||
rows, err := ds.chatRoomDb.Query("SELECT m_nsUsrName, nickname FROM GroupMember")
|
||||
rows, err := ds.chatRoomDb.Query("SELECT m_nsUsrName, IFNULL(nickname,\"\") FROM GroupMember")
|
||||
if err != nil {
|
||||
log.Printf("警告: 获取群聊成员失败: %v", err)
|
||||
log.Err(err).Msgf("数据库 %s 获取群聊成员失败", files[0])
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -148,7 +154,7 @@ func (ds *DataSource) initChatRoomDb(path string) error {
|
||||
var user string
|
||||
var nickName string
|
||||
if err := rows.Scan(&user, &nickName); err != nil {
|
||||
log.Printf("警告: 扫描表名失败: %v", err)
|
||||
log.Err(err).Msgf("数据库 %s 扫描表名失败", files[0])
|
||||
continue
|
||||
}
|
||||
ds.user2DisplayName[user] = nickName
|
||||
@@ -161,14 +167,29 @@ func (ds *DataSource) initChatRoomDb(path string) error {
|
||||
func (ds *DataSource) initSessionDb(path string) error {
|
||||
files, err := util.FindFilesWithPatterns(path, SessionFilePattern, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查找最近会话数据库文件失败: %w", err)
|
||||
return errors.DBFileNotFound(path, SessionFilePattern, err)
|
||||
}
|
||||
if len(files) == 0 {
|
||||
return fmt.Errorf("未找到最近会话数据库文件: %s", path)
|
||||
return errors.DBFileNotFound(path, SessionFilePattern, nil)
|
||||
}
|
||||
ds.sessionDb, err = sql.Open("sqlite3", files[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("连接最近会话数据库失败: %w", err)
|
||||
return errors.DBConnectFailed(files[0], err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *DataSource) initMediaDb(path string) error {
|
||||
files, err := util.FindFilesWithPatterns(path, MediaFilePattern, true)
|
||||
if err != nil {
|
||||
return errors.DBFileNotFound(path, MediaFilePattern, err)
|
||||
}
|
||||
if len(files) == 0 {
|
||||
return errors.DBFileNotFound(path, MediaFilePattern, nil)
|
||||
}
|
||||
ds.mediaDb, err = sql.Open("sqlite3", files[0])
|
||||
if err != nil {
|
||||
return errors.DBConnectFailed(files[0], err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -178,20 +199,20 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T
|
||||
// 在 darwinv3 中,每个联系人/群聊的消息存储在单独的表中,表名为 Chat_md5(talker)
|
||||
// 首先需要找到对应的表名
|
||||
if talker == "" {
|
||||
return nil, fmt.Errorf("talker 不能为空")
|
||||
return nil, errors.ErrTalkerEmpty
|
||||
}
|
||||
|
||||
_talkerMd5Bytes := md5.Sum([]byte(talker))
|
||||
talkerMd5 := hex.EncodeToString(_talkerMd5Bytes[:])
|
||||
db, ok := ds.talkerDBMap[talkerMd5]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("未找到 talker %s 的消息数据库", talker)
|
||||
return nil, errors.TalkerNotFound(talker)
|
||||
}
|
||||
tableName := fmt.Sprintf("Chat_%s", talkerMd5)
|
||||
|
||||
// 构建查询条件
|
||||
query := fmt.Sprintf(`
|
||||
SELECT msgCreateTime, msgContent, messageType, mesDes, msgSource, CompressContent, ConBlob
|
||||
SELECT msgCreateTime, msgContent, messageType, mesDes
|
||||
FROM %s
|
||||
WHERE msgCreateTime >= ? AND msgCreateTime <= ?
|
||||
ORDER BY msgCreateTime ASC
|
||||
@@ -208,7 +229,7 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T
|
||||
// 执行查询
|
||||
rows, err := db.QueryContext(ctx, query, startTime.Unix(), endTime.Unix())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询表 %s 失败: %w", tableName, err)
|
||||
return nil, errors.QueryFailed(query, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -216,18 +237,14 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T
|
||||
messages := []*model.Message{}
|
||||
for rows.Next() {
|
||||
var msg model.MessageDarwinV3
|
||||
var compressContent, conBlob []byte
|
||||
err := rows.Scan(
|
||||
&msg.MesCreateTime,
|
||||
&msg.MesContent,
|
||||
&msg.MesType,
|
||||
&msg.MsgCreateTime,
|
||||
&msg.MsgContent,
|
||||
&msg.MessageType,
|
||||
&msg.MesDes,
|
||||
&msg.MesSource,
|
||||
&compressContent,
|
||||
&conBlob,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("警告: 扫描消息行失败: %v", err)
|
||||
log.Err(err).Msgf("扫描消息行失败")
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -260,13 +277,13 @@ func (ds *DataSource) GetContacts(ctx context.Context, key string, limit, offset
|
||||
|
||||
if key != "" {
|
||||
// 按照关键字查询
|
||||
query = `SELECT m_nsUsrName, nickname, IFNULL(m_nsRemark,""), m_uiSex, IFNULL(m_nsAliasName,"")
|
||||
query = `SELECT IFNULL(m_nsUsrName,""), IFNULL(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,"")
|
||||
query = `SELECT IFNULL(m_nsUsrName,""), IFNULL(nickname,""), IFNULL(m_nsRemark,""), m_uiSex, IFNULL(m_nsAliasName,"")
|
||||
FROM WCContact`
|
||||
}
|
||||
|
||||
@@ -282,7 +299,7 @@ func (ds *DataSource) GetContacts(ctx context.Context, key string, limit, offset
|
||||
// 执行查询
|
||||
rows, err := ds.contactDb.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询联系人失败: %w", err)
|
||||
return nil, errors.QueryFailed(query, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -298,7 +315,7 @@ func (ds *DataSource) GetContacts(ctx context.Context, key string, limit, offset
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("扫描联系人行失败: %w", err)
|
||||
return nil, errors.ScanRowFailed(err)
|
||||
}
|
||||
|
||||
contacts = append(contacts, contactDarwinV3.Wrap())
|
||||
@@ -314,13 +331,13 @@ func (ds *DataSource) GetChatRooms(ctx context.Context, key string, limit, offse
|
||||
|
||||
if key != "" {
|
||||
// 按照关键字查询
|
||||
query = `SELECT m_nsUsrName, nickname, IFNULL(m_nsRemark,""), IFNULL(m_nsChatRoomMemList,""), IFNULL(m_nsChatRoomAdminList,"")
|
||||
query = `SELECT IFNULL(m_nsUsrName,""), IFNULL(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,"")
|
||||
query = `SELECT IFNULL(m_nsUsrName,""), IFNULL(nickname,""), IFNULL(m_nsRemark,""), IFNULL(m_nsChatRoomMemList,""), IFNULL(m_nsChatRoomAdminList,"")
|
||||
FROM GroupContact`
|
||||
}
|
||||
|
||||
@@ -336,7 +353,7 @@ func (ds *DataSource) GetChatRooms(ctx context.Context, key string, limit, offse
|
||||
// 执行查询
|
||||
rows, err := ds.chatRoomDb.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询群聊失败: %w", err)
|
||||
return nil, errors.QueryFailed(query, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -352,7 +369,7 @@ func (ds *DataSource) GetChatRooms(ctx context.Context, key string, limit, offse
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("扫描群聊行失败: %w", err)
|
||||
return nil, errors.ScanRowFailed(err)
|
||||
}
|
||||
|
||||
chatRooms = append(chatRooms, chatRoomDarwinV3.Wrap(ds.user2DisplayName))
|
||||
@@ -364,13 +381,13 @@ func (ds *DataSource) GetChatRooms(ctx context.Context, key string, limit, offse
|
||||
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
|
||||
`SELECT IFNULL(m_nsUsrName,""), IFNULL(nickname,""), IFNULL(m_nsRemark,""), IFNULL(m_nsChatRoomMemList,""), IFNULL(m_nsChatRoomAdminList,"")
|
||||
FROM GroupContact
|
||||
WHERE m_nsUsrName = ?`,
|
||||
contacts[0].UserName)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询群聊失败: %w", err)
|
||||
return nil, errors.QueryFailed(query, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -385,7 +402,7 @@ func (ds *DataSource) GetChatRooms(ctx context.Context, key string, limit, offse
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("扫描群聊行失败: %w", err)
|
||||
return nil, errors.ScanRowFailed(err)
|
||||
}
|
||||
|
||||
chatRooms = append(chatRooms, chatRoomDarwinV3.Wrap(ds.user2DisplayName))
|
||||
@@ -433,7 +450,7 @@ func (ds *DataSource) GetSessions(ctx context.Context, key string, limit, offset
|
||||
// 执行查询
|
||||
rows, err := ds.sessionDb.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询会话失败: %w", err)
|
||||
return nil, errors.QueryFailed(query, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -446,7 +463,7 @@ func (ds *DataSource) GetSessions(ctx context.Context, key string, limit, offset
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("扫描会话行失败: %w", err)
|
||||
return nil, errors.ScanRowFailed(err)
|
||||
}
|
||||
|
||||
// 包装成通用模型
|
||||
@@ -470,40 +487,99 @@ func (ds *DataSource) GetSessions(ctx context.Context, key string, limit, offset
|
||||
return sessions, nil
|
||||
}
|
||||
|
||||
func (ds *DataSource) GetMedia(ctx context.Context, _type string, key string) (*model.Media, error) {
|
||||
if key == "" {
|
||||
return nil, errors.ErrKeyEmpty
|
||||
}
|
||||
query := `SELECT
|
||||
r.mediaMd5,
|
||||
r.mediaSize,
|
||||
r.inodeNumber,
|
||||
r.modifyTime,
|
||||
d.relativePath,
|
||||
d.fileName
|
||||
FROM
|
||||
HlinkMediaRecord r
|
||||
JOIN
|
||||
HlinkMediaDetail d ON r.inodeNumber = d.inodeNumber
|
||||
WHERE
|
||||
r.mediaMd5 = ?`
|
||||
args := []interface{}{key}
|
||||
// 执行查询
|
||||
rows, err := ds.mediaDb.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, errors.QueryFailed(query, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var media *model.Media
|
||||
for rows.Next() {
|
||||
var mediaDarwinV3 model.MediaDarwinV3
|
||||
err := rows.Scan(
|
||||
&mediaDarwinV3.MediaMd5,
|
||||
&mediaDarwinV3.MediaSize,
|
||||
&mediaDarwinV3.InodeNumber,
|
||||
&mediaDarwinV3.ModifyTime,
|
||||
&mediaDarwinV3.RelativePath,
|
||||
&mediaDarwinV3.FileName,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, errors.ScanRowFailed(err)
|
||||
}
|
||||
|
||||
// 包装成通用模型
|
||||
media = mediaDarwinV3.Wrap()
|
||||
}
|
||||
|
||||
if media == nil {
|
||||
return nil, errors.ErrMediaNotFound
|
||||
}
|
||||
|
||||
return media, nil
|
||||
}
|
||||
|
||||
// Close 实现关闭数据库连接的方法
|
||||
func (ds *DataSource) Close() error {
|
||||
var errs []error
|
||||
|
||||
// 关闭消息数据库连接
|
||||
for i, db := range ds.messageDbs {
|
||||
for _, db := range ds.messageDbs {
|
||||
if err := db.Close(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("关闭消息数据库 %d 失败: %w", i, err))
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭联系人数据库连接
|
||||
if ds.contactDb != nil {
|
||||
if err := ds.contactDb.Close(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("关闭联系人数据库失败: %w", err))
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭群聊数据库连接
|
||||
if ds.chatRoomDb != nil {
|
||||
if err := ds.chatRoomDb.Close(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("关闭群聊数据库失败: %w", err))
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭会话数据库连接
|
||||
if ds.sessionDb != nil {
|
||||
if err := ds.sessionDb.Close(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("关闭会话数据库失败: %w", err))
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭媒体数据库连接
|
||||
if ds.mediaDb != nil {
|
||||
if err := ds.mediaDb.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("关闭数据库连接时发生错误: %v", errs)
|
||||
return errors.DBCloseFailed(errs[0])
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -2,20 +2,15 @@ package datasource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/errors"
|
||||
"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 {
|
||||
|
||||
// 消息
|
||||
@@ -30,10 +25,13 @@ type DataSource interface {
|
||||
// 最近会话
|
||||
GetSessions(ctx context.Context, key string, limit, offset int) ([]*model.Session, error)
|
||||
|
||||
// 媒体
|
||||
GetMedia(ctx context.Context, _type string, key string) (*model.Media, error)
|
||||
|
||||
Close() error
|
||||
}
|
||||
|
||||
func NewDataSource(path string, platform string, version int) (DataSource, error) {
|
||||
func New(path string, platform string, version int) (DataSource, error) {
|
||||
switch {
|
||||
case platform == "windows" && version == 3:
|
||||
return windowsv3.New(path)
|
||||
@@ -44,6 +42,6 @@ func NewDataSource(path string, platform string, version int) (DataSource, error
|
||||
case platform == "darwin" && version == 4:
|
||||
return v4.New(path)
|
||||
default:
|
||||
return nil, fmt.Errorf("%w: %s v%d", ErrUnsupportedPlatform, platform, version)
|
||||
return nil, errors.PlatformUnsupported(platform, version)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,21 +6,23 @@ import (
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/errors"
|
||||
"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$"
|
||||
MediaFilePattern = "^hardlink\\.db$"
|
||||
)
|
||||
|
||||
// MessageDBInfo 存储消息数据库的信息
|
||||
@@ -28,7 +30,6 @@ type MessageDBInfo struct {
|
||||
FilePath string
|
||||
StartTime time.Time
|
||||
EndTime time.Time
|
||||
ID2Name map[int]string
|
||||
}
|
||||
|
||||
type DataSource struct {
|
||||
@@ -36,6 +37,7 @@ type DataSource struct {
|
||||
messageDbs map[string]*sql.DB
|
||||
contactDb *sql.DB
|
||||
sessionDb *sql.DB
|
||||
mediaDb *sql.DB
|
||||
|
||||
// 消息数据库信息
|
||||
messageFiles []MessageDBInfo
|
||||
@@ -49,13 +51,16 @@ func New(path string) (*DataSource, error) {
|
||||
}
|
||||
|
||||
if err := ds.initMessageDbs(path); err != nil {
|
||||
return nil, fmt.Errorf("初始化消息数据库失败: %w", err)
|
||||
return nil, errors.DBInitFailed(err)
|
||||
}
|
||||
if err := ds.initContactDb(path); err != nil {
|
||||
return nil, fmt.Errorf("初始化联系人数据库失败: %w", err)
|
||||
return nil, errors.DBInitFailed(err)
|
||||
}
|
||||
if err := ds.initSessionDb(path); err != nil {
|
||||
return nil, fmt.Errorf("初始化会话数据库失败: %w", err)
|
||||
return nil, errors.DBInitFailed(err)
|
||||
}
|
||||
if err := ds.initMediaDb(path); err != nil {
|
||||
return nil, errors.DBInitFailed(err)
|
||||
}
|
||||
|
||||
return ds, nil
|
||||
@@ -65,11 +70,11 @@ func (ds *DataSource) initMessageDbs(path string) error {
|
||||
// 查找所有消息数据库文件
|
||||
files, err := util.FindFilesWithPatterns(path, MessageFilePattern, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查找消息数据库文件失败: %w", err)
|
||||
return errors.DBFileNotFound(path, MessageFilePattern, err)
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return fmt.Errorf("未找到任何消息数据库文件: %s", path)
|
||||
return errors.DBFileNotFound(path, MessageFilePattern, nil)
|
||||
}
|
||||
|
||||
// 处理每个数据库文件
|
||||
@@ -77,7 +82,7 @@ func (ds *DataSource) initMessageDbs(path string) error {
|
||||
// 连接数据库
|
||||
db, err := sql.Open("sqlite3", filePath)
|
||||
if err != nil {
|
||||
log.Printf("警告: 连接数据库 %s 失败: %v", filePath, err)
|
||||
log.Err(err).Msgf("连接数据库 %s 失败", filePath)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -87,38 +92,16 @@ func (ds *DataSource) initMessageDbs(path string) error {
|
||||
|
||||
row := db.QueryRow("SELECT timestamp FROM Timestamp LIMIT 1")
|
||||
if err := row.Scan(×tamp); err != nil {
|
||||
log.Printf("警告: 获取数据库 %s 的时间戳失败: %v", filePath, err)
|
||||
log.Err(err).Msgf("获取数据库 %s 的时间戳失败", filePath)
|
||||
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,
|
||||
})
|
||||
|
||||
// 保存数据库连接
|
||||
@@ -145,16 +128,16 @@ func (ds *DataSource) initMessageDbs(path string) error {
|
||||
func (ds *DataSource) initContactDb(path string) error {
|
||||
files, err := util.FindFilesWithPatterns(path, ContactFilePattern, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查找联系人数据库文件失败: %w", err)
|
||||
return errors.DBFileNotFound(path, ContactFilePattern, err)
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return fmt.Errorf("未找到联系人数据库文件: %s", path)
|
||||
return errors.DBFileNotFound(path, ContactFilePattern, nil)
|
||||
}
|
||||
|
||||
ds.contactDb, err = sql.Open("sqlite3", files[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("连接联系人数据库失败: %w", err)
|
||||
return errors.DBConnectFailed(files[0], err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -163,14 +146,29 @@ func (ds *DataSource) initContactDb(path string) error {
|
||||
func (ds *DataSource) initSessionDb(path string) error {
|
||||
files, err := util.FindFilesWithPatterns(path, SessionFilePattern, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查找最近会话数据库文件失败: %w", err)
|
||||
return errors.DBFileNotFound(path, SessionFilePattern, err)
|
||||
}
|
||||
if len(files) == 0 {
|
||||
return fmt.Errorf("未找到最近会话数据库文件: %s", path)
|
||||
return errors.DBFileNotFound(path, SessionFilePattern, nil)
|
||||
}
|
||||
ds.sessionDb, err = sql.Open("sqlite3", files[0])
|
||||
if err != nil {
|
||||
return fmt.Errorf("连接最近会话数据库失败: %w", err)
|
||||
return errors.DBConnectFailed(files[0], err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (ds *DataSource) initMediaDb(path string) error {
|
||||
files, err := util.FindFilesWithPatterns(path, MediaFilePattern, true)
|
||||
if err != nil {
|
||||
return errors.DBFileNotFound(path, MediaFilePattern, err)
|
||||
}
|
||||
if len(files) == 0 {
|
||||
return errors.DBFileNotFound(path, MediaFilePattern, nil)
|
||||
}
|
||||
ds.mediaDb, err = sql.Open("sqlite3", files[0])
|
||||
if err != nil {
|
||||
return errors.DBConnectFailed(files[0], err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -188,13 +186,13 @@ func (ds *DataSource) getDBInfosForTimeRange(startTime, endTime time.Time) []Mes
|
||||
|
||||
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 参数")
|
||||
return nil, errors.ErrTalkerEmpty
|
||||
}
|
||||
|
||||
// 找到时间范围内的数据库文件
|
||||
dbInfos := ds.getDBInfosForTimeRange(startTime, endTime)
|
||||
if len(dbInfos) == 0 {
|
||||
return nil, fmt.Errorf("未找到时间范围 %v 到 %v 内的数据库文件", startTime, endTime)
|
||||
return nil, errors.TimeRangeNotFound(startTime, endTime)
|
||||
}
|
||||
|
||||
if len(dbInfos) == 1 {
|
||||
@@ -213,13 +211,13 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T
|
||||
|
||||
db, ok := ds.messageDbs[dbInfo.FilePath]
|
||||
if !ok {
|
||||
log.Printf("警告: 数据库 %s 未打开", dbInfo.FilePath)
|
||||
log.Error().Msgf("数据库 %s 未打开", dbInfo.FilePath)
|
||||
continue
|
||||
}
|
||||
|
||||
messages, err := ds.getMessagesFromDB(ctx, db, dbInfo, startTime, endTime, talker)
|
||||
if err != nil {
|
||||
log.Printf("警告: 从数据库 %s 获取消息失败: %v", dbInfo.FilePath, err)
|
||||
log.Err(err).Msgf("从数据库 %s 获取消息失败", dbInfo.FilePath)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -232,7 +230,7 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T
|
||||
|
||||
// 对所有消息按时间排序
|
||||
sort.Slice(totalMessages, func(i, j int) bool {
|
||||
return totalMessages[i].Sequence < totalMessages[j].Sequence
|
||||
return totalMessages[i].Seq < totalMessages[j].Seq
|
||||
})
|
||||
|
||||
// 处理分页
|
||||
@@ -254,7 +252,7 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T
|
||||
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)
|
||||
return nil, errors.DBConnectFailed(dbInfo.FilePath, nil)
|
||||
}
|
||||
|
||||
// 构建表名
|
||||
@@ -267,10 +265,11 @@ func (ds *DataSource) getMessagesSingleFile(ctx context.Context, dbInfo MessageD
|
||||
args := []interface{}{startTime.Unix(), endTime.Unix()}
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
SELECT sort_seq, local_type, real_sender_id, create_time, message_content, packed_info_data, status
|
||||
FROM %s
|
||||
SELECT m.sort_seq, m.local_type, n.user_name, m.create_time, m.message_content, m.packed_info_data, m.status
|
||||
FROM %s m
|
||||
LEFT JOIN Name2Id n ON m.real_sender_id = n.rowid
|
||||
WHERE %s
|
||||
ORDER BY sort_seq ASC
|
||||
ORDER BY m.sort_seq ASC
|
||||
`, tableName, strings.Join(conditions, " AND "))
|
||||
|
||||
if limit > 0 {
|
||||
@@ -283,30 +282,29 @@ func (ds *DataSource) getMessagesSingleFile(ctx context.Context, dbInfo MessageD
|
||||
// 执行查询
|
||||
rows, err := db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询数据库 %s 失败: %w", dbInfo.FilePath, err)
|
||||
return nil, errors.QueryFailed(query, 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.UserName,
|
||||
&msg.CreateTime,
|
||||
&msg.MessageContent,
|
||||
&msg.PackedInfoData,
|
||||
&msg.Status,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("扫描消息行失败: %w", err)
|
||||
return nil, errors.ScanRowFailed(err)
|
||||
}
|
||||
|
||||
messages = append(messages, msg.Wrap(dbInfo.ID2Name, isChatRoom))
|
||||
messages = append(messages, msg.Wrap(talker))
|
||||
}
|
||||
|
||||
return messages, nil
|
||||
@@ -330,7 +328,7 @@ func (ds *DataSource) getMessagesFromDB(ctx context.Context, db *sql.DB, dbInfo
|
||||
// 表不存在,返回空结果
|
||||
return []*model.Message{}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("检查表 %s 是否存在失败: %w", tableName, err)
|
||||
return nil, errors.QueryFailed("", err)
|
||||
}
|
||||
|
||||
// 构建查询条件
|
||||
@@ -338,10 +336,11 @@ func (ds *DataSource) getMessagesFromDB(ctx context.Context, db *sql.DB, dbInfo
|
||||
args := []interface{}{startTime.Unix(), endTime.Unix()}
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
SELECT sort_seq, local_type, real_sender_id, create_time, message_content, packed_info_data, status
|
||||
FROM %s
|
||||
SELECT m.sort_seq, m.local_type, n.user_name, m.create_time, m.message_content, m.packed_info_data, m.status
|
||||
FROM %s m
|
||||
LEFT JOIN Name2Id n ON m.real_sender_id = n.rowid
|
||||
WHERE %s
|
||||
ORDER BY sort_seq ASC
|
||||
ORDER BY m.sort_seq ASC
|
||||
`, tableName, strings.Join(conditions, " AND "))
|
||||
|
||||
// 执行查询
|
||||
@@ -351,30 +350,29 @@ func (ds *DataSource) getMessagesFromDB(ctx context.Context, db *sql.DB, dbInfo
|
||||
if strings.Contains(err.Error(), "no such table") {
|
||||
return []*model.Message{}, nil
|
||||
}
|
||||
return nil, fmt.Errorf("查询数据库失败: %w", err)
|
||||
return nil, errors.QueryFailed(query, 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.UserName,
|
||||
&msg.CreateTime,
|
||||
&msg.MessageContent,
|
||||
&msg.PackedInfoData,
|
||||
&msg.Status,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("扫描消息行失败: %w", err)
|
||||
return nil, errors.ScanRowFailed(err)
|
||||
}
|
||||
|
||||
messages = append(messages, msg.Wrap(dbInfo.ID2Name, isChatRoom))
|
||||
messages = append(messages, msg.Wrap(talker))
|
||||
}
|
||||
|
||||
return messages, nil
|
||||
@@ -408,7 +406,7 @@ func (ds *DataSource) GetContacts(ctx context.Context, key string, limit, offset
|
||||
// 执行查询
|
||||
rows, err := ds.contactDb.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询联系人失败: %w", err)
|
||||
return nil, errors.QueryFailed(query, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -424,7 +422,7 @@ func (ds *DataSource) GetContacts(ctx context.Context, key string, limit, offset
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("扫描联系人行失败: %w", err)
|
||||
return nil, errors.ScanRowFailed(err)
|
||||
}
|
||||
|
||||
contacts = append(contacts, contactV4.Wrap())
|
||||
@@ -446,7 +444,7 @@ func (ds *DataSource) GetChatRooms(ctx context.Context, key string, limit, offse
|
||||
// 执行查询
|
||||
rows, err := ds.contactDb.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询群聊失败: %w", err)
|
||||
return nil, errors.QueryFailed(query, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -460,7 +458,7 @@ func (ds *DataSource) GetChatRooms(ctx context.Context, key string, limit, offse
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("扫描群聊行失败: %w", err)
|
||||
return nil, errors.ScanRowFailed(err)
|
||||
}
|
||||
|
||||
chatRooms = append(chatRooms, chatRoomV4.Wrap())
|
||||
@@ -476,7 +474,7 @@ func (ds *DataSource) GetChatRooms(ctx context.Context, key string, limit, offse
|
||||
contacts[0].UserName)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询群聊失败: %w", err)
|
||||
return nil, errors.QueryFailed(query, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -489,7 +487,7 @@ func (ds *DataSource) GetChatRooms(ctx context.Context, key string, limit, offse
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("扫描群聊行失败: %w", err)
|
||||
return nil, errors.ScanRowFailed(err)
|
||||
}
|
||||
|
||||
chatRooms = append(chatRooms, chatRoomV4.Wrap())
|
||||
@@ -523,7 +521,7 @@ func (ds *DataSource) GetChatRooms(ctx context.Context, key string, limit, offse
|
||||
// 执行查询
|
||||
rows, err := ds.contactDb.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询群聊失败: %w", err)
|
||||
return nil, errors.QueryFailed(query, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -537,7 +535,7 @@ func (ds *DataSource) GetChatRooms(ctx context.Context, key string, limit, offse
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("扫描群聊行失败: %w", err)
|
||||
return nil, errors.ScanRowFailed(err)
|
||||
}
|
||||
|
||||
chatRooms = append(chatRooms, chatRoomV4.Wrap())
|
||||
@@ -577,7 +575,7 @@ func (ds *DataSource) GetSessions(ctx context.Context, key string, limit, offset
|
||||
// 执行查询
|
||||
rows, err := ds.sessionDb.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询会话失败: %w", err)
|
||||
return nil, errors.QueryFailed(query, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -593,7 +591,7 @@ func (ds *DataSource) GetSessions(ctx context.Context, key string, limit, offset
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("扫描会话行失败: %w", err)
|
||||
return nil, errors.ScanRowFailed(err)
|
||||
}
|
||||
|
||||
sessions = append(sessions, sessionV4.Wrap())
|
||||
@@ -602,32 +600,113 @@ func (ds *DataSource) GetSessions(ctx context.Context, key string, limit, offset
|
||||
return sessions, nil
|
||||
}
|
||||
|
||||
func (ds *DataSource) GetMedia(ctx context.Context, _type string, key string) (*model.Media, error) {
|
||||
if key == "" {
|
||||
return nil, errors.ErrKeyEmpty
|
||||
}
|
||||
|
||||
if len(key) != 32 {
|
||||
return nil, errors.ErrKeyLengthMust32
|
||||
}
|
||||
|
||||
var table string
|
||||
switch _type {
|
||||
case "image":
|
||||
table = "image_hardlink_info_v3"
|
||||
case "video":
|
||||
table = "video_hardlink_info_v3"
|
||||
case "file":
|
||||
table = "file_hardlink_info_v3"
|
||||
default:
|
||||
return nil, errors.MediaTypeUnsupported(_type)
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
SELECT
|
||||
f.md5,
|
||||
f.file_name,
|
||||
f.file_size,
|
||||
f.modify_time,
|
||||
IFNULL(d1.username,""),
|
||||
IFNULL(d2.username,"")
|
||||
FROM
|
||||
%s f
|
||||
LEFT JOIN
|
||||
dir2id d1 ON d1.rowid = f.dir1
|
||||
LEFT JOIN
|
||||
dir2id d2 ON d2.rowid = f.dir2
|
||||
`, table)
|
||||
query += " WHERE f.md5 = ? OR f.file_name LIKE ? || '%'"
|
||||
args := []interface{}{key, key}
|
||||
|
||||
rows, err := ds.mediaDb.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, errors.QueryFailed(query, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var media *model.Media
|
||||
for rows.Next() {
|
||||
var mediaV4 model.MediaV4
|
||||
err := rows.Scan(
|
||||
&mediaV4.Key,
|
||||
&mediaV4.Name,
|
||||
&mediaV4.Size,
|
||||
&mediaV4.ModifyTime,
|
||||
&mediaV4.Dir1,
|
||||
&mediaV4.Dir2,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.ScanRowFailed(err)
|
||||
}
|
||||
mediaV4.Type = _type
|
||||
media = mediaV4.Wrap()
|
||||
|
||||
// 跳过缩略图
|
||||
if _type == "image" && !strings.Contains(media.Name, "_t") {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if media == nil {
|
||||
return nil, errors.ErrMediaNotFound
|
||||
}
|
||||
|
||||
return media, nil
|
||||
}
|
||||
|
||||
func (ds *DataSource) Close() error {
|
||||
var errs []error
|
||||
|
||||
// 关闭消息数据库连接
|
||||
for path, db := range ds.messageDbs {
|
||||
for _, db := range ds.messageDbs {
|
||||
if err := db.Close(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("关闭消息数据库 %s 失败: %w", path, err))
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭联系人数据库连接
|
||||
if ds.contactDb != nil {
|
||||
if err := ds.contactDb.Close(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("关闭联系人数据库失败: %w", err))
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭会话数据库连接
|
||||
if ds.sessionDb != nil {
|
||||
if err := ds.sessionDb.Close(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("关闭会话数据库失败: %w", err))
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if ds.mediaDb != nil {
|
||||
if err := ds.mediaDb.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("关闭数据库连接时发生错误: %v", errs)
|
||||
return errors.DBCloseFailed(errs[0])
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -3,21 +3,26 @@ package windowsv3
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"log"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"github.com/rs/zerolog/log"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/errors"
|
||||
"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$"
|
||||
ImageFilePattern = "^HardLinkImage\\.db$"
|
||||
VideoFilePattern = "^HardLinkVideo\\.db$"
|
||||
FileFilePattern = "^HardLinkFile\\.db$"
|
||||
)
|
||||
|
||||
// MessageDBInfo 保存消息数据库的信息
|
||||
@@ -37,6 +42,10 @@ type DataSource struct {
|
||||
// 联系人数据库
|
||||
contactDbFile string
|
||||
contactDb *sql.DB
|
||||
|
||||
imageDb *sql.DB
|
||||
videoDb *sql.DB
|
||||
fileDb *sql.DB
|
||||
}
|
||||
|
||||
// New 创建一个新的 WindowsV3DataSource
|
||||
@@ -48,12 +57,16 @@ func New(path string) (*DataSource, error) {
|
||||
|
||||
// 初始化消息数据库
|
||||
if err := ds.initMessageDbs(path); err != nil {
|
||||
return nil, fmt.Errorf("初始化消息数据库失败: %w", err)
|
||||
return nil, errors.DBInitFailed(err)
|
||||
}
|
||||
|
||||
// 初始化联系人数据库
|
||||
if err := ds.initContactDb(path); err != nil {
|
||||
return nil, fmt.Errorf("初始化联系人数据库失败: %w", err)
|
||||
return nil, errors.DBInitFailed(err)
|
||||
}
|
||||
|
||||
if err := ds.initMediaDb(path); err != nil {
|
||||
return nil, errors.DBInitFailed(err)
|
||||
}
|
||||
|
||||
return ds, nil
|
||||
@@ -64,11 +77,11 @@ func (ds *DataSource) initMessageDbs(path string) error {
|
||||
// 查找所有消息数据库文件
|
||||
files, err := util.FindFilesWithPatterns(path, MessageFilePattern, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查找消息数据库文件失败: %w", err)
|
||||
return errors.DBFileNotFound(path, MessageFilePattern, err)
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return fmt.Errorf("未找到任何消息数据库文件: %s", path)
|
||||
return errors.DBFileNotFound(path, MessageFilePattern, nil)
|
||||
}
|
||||
|
||||
// 处理每个数据库文件
|
||||
@@ -76,7 +89,7 @@ func (ds *DataSource) initMessageDbs(path string) error {
|
||||
// 连接数据库
|
||||
db, err := sql.Open("sqlite3", filePath)
|
||||
if err != nil {
|
||||
log.Printf("警告: 连接数据库 %s 失败: %v", filePath, err)
|
||||
log.Err(err).Msgf("连接数据库 %s 失败", filePath)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -85,7 +98,7 @@ func (ds *DataSource) initMessageDbs(path string) error {
|
||||
|
||||
rows, err := db.Query("SELECT tableIndex, tableVersion, tableDesc FROM DBInfo")
|
||||
if err != nil {
|
||||
log.Printf("警告: 查询数据库 %s 的 DBInfo 表失败: %v", filePath, err)
|
||||
log.Err(err).Msgf("查询数据库 %s 的 DBInfo 表失败", filePath)
|
||||
db.Close()
|
||||
continue
|
||||
}
|
||||
@@ -96,7 +109,7 @@ func (ds *DataSource) initMessageDbs(path string) error {
|
||||
var tableDesc string
|
||||
|
||||
if err := rows.Scan(&tableIndex, &tableVersion, &tableDesc); err != nil {
|
||||
log.Printf("警告: 扫描 DBInfo 行失败: %v", err)
|
||||
log.Err(err).Msg("扫描 DBInfo 行失败")
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -112,7 +125,7 @@ func (ds *DataSource) initMessageDbs(path string) error {
|
||||
talkerMap := make(map[string]int)
|
||||
rows, err = db.Query("SELECT UsrName FROM Name2ID")
|
||||
if err != nil {
|
||||
log.Printf("警告: 查询数据库 %s 的 Name2ID 表失败: %v", filePath, err)
|
||||
log.Err(err).Msgf("查询数据库 %s 的 Name2ID 表失败", filePath)
|
||||
db.Close()
|
||||
continue
|
||||
}
|
||||
@@ -121,7 +134,7 @@ func (ds *DataSource) initMessageDbs(path string) error {
|
||||
for rows.Next() {
|
||||
var userName string
|
||||
if err := rows.Scan(&userName); err != nil {
|
||||
log.Printf("警告: 扫描 Name2ID 行失败: %v", err)
|
||||
log.Err(err).Msg("扫描 Name2ID 行失败")
|
||||
continue
|
||||
}
|
||||
talkerMap[userName] = i
|
||||
@@ -161,18 +174,65 @@ func (ds *DataSource) initMessageDbs(path string) error {
|
||||
func (ds *DataSource) initContactDb(path string) error {
|
||||
files, err := util.FindFilesWithPatterns(path, ContactFilePattern, true)
|
||||
if err != nil {
|
||||
return fmt.Errorf("查找联系人数据库文件失败: %w", err)
|
||||
return errors.DBFileNotFound(path, ContactFilePattern, err)
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return fmt.Errorf("未找到联系人数据库文件: %s", path)
|
||||
return errors.DBFileNotFound(path, ContactFilePattern, nil)
|
||||
}
|
||||
|
||||
ds.contactDbFile = files[0]
|
||||
|
||||
ds.contactDb, err = sql.Open("sqlite3", ds.contactDbFile)
|
||||
if err != nil {
|
||||
return fmt.Errorf("连接联系人数据库失败: %w", err)
|
||||
return errors.DBConnectFailed(ds.contactDbFile, err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// initContactDb 初始化联系人数据库
|
||||
func (ds *DataSource) initMediaDb(path string) error {
|
||||
files, err := util.FindFilesWithPatterns(path, ImageFilePattern, true)
|
||||
if err != nil {
|
||||
return errors.DBFileNotFound(path, ImageFilePattern, err)
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return errors.DBFileNotFound(path, ImageFilePattern, nil)
|
||||
}
|
||||
|
||||
ds.imageDb, err = sql.Open("sqlite3", files[0])
|
||||
if err != nil {
|
||||
return errors.DBConnectFailed(files[0], err)
|
||||
}
|
||||
|
||||
files, err = util.FindFilesWithPatterns(path, VideoFilePattern, true)
|
||||
if err != nil {
|
||||
return errors.DBFileNotFound(path, VideoFilePattern, err)
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return errors.DBFileNotFound(path, VideoFilePattern, nil)
|
||||
}
|
||||
|
||||
ds.videoDb, err = sql.Open("sqlite3", files[0])
|
||||
if err != nil {
|
||||
return errors.DBConnectFailed(files[0], err)
|
||||
}
|
||||
|
||||
files, err = util.FindFilesWithPatterns(path, FileFilePattern, true)
|
||||
if err != nil {
|
||||
return errors.DBFileNotFound(path, FileFilePattern, err)
|
||||
}
|
||||
|
||||
if len(files) == 0 {
|
||||
return errors.DBFileNotFound(path, FileFilePattern, nil)
|
||||
}
|
||||
|
||||
ds.fileDb, err = sql.Open("sqlite3", files[0])
|
||||
if err != nil {
|
||||
return errors.DBConnectFailed(files[0], err)
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -194,7 +254,7 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T
|
||||
// 找到时间范围内的数据库文件
|
||||
dbInfos := ds.getDBInfosForTimeRange(startTime, endTime)
|
||||
if len(dbInfos) == 0 {
|
||||
return nil, fmt.Errorf("未找到时间范围 %v 到 %v 内的数据库文件", startTime, endTime)
|
||||
return nil, errors.TimeRangeNotFound(startTime, endTime)
|
||||
}
|
||||
|
||||
if len(dbInfos) == 1 {
|
||||
@@ -213,7 +273,7 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T
|
||||
|
||||
db, ok := ds.messageDbs[dbInfo.FilePath]
|
||||
if !ok {
|
||||
log.Printf("警告: 数据库 %s 未打开", dbInfo.FilePath)
|
||||
log.Error().Msgf("数据库 %s 未打开", dbInfo.FilePath)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -233,7 +293,7 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
SELECT Sequence, CreateTime, TalkerId, StrTalker, IsSender,
|
||||
SELECT Sequence, CreateTime, StrTalker, IsSender,
|
||||
Type, SubType, StrContent, CompressContent, BytesExtra
|
||||
FROM MSG
|
||||
WHERE %s
|
||||
@@ -243,7 +303,7 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T
|
||||
// 执行查询
|
||||
rows, err := db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
log.Printf("警告: 查询数据库 %s 失败: %v", dbInfo.FilePath, err)
|
||||
log.Err(err).Msgf("查询数据库 %s 失败", dbInfo.FilePath)
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -256,7 +316,6 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T
|
||||
err := rows.Scan(
|
||||
&msg.Sequence,
|
||||
&msg.CreateTime,
|
||||
&msg.TalkerID,
|
||||
&msg.StrTalker,
|
||||
&msg.IsSender,
|
||||
&msg.Type,
|
||||
@@ -266,7 +325,7 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T
|
||||
&bytesExtra,
|
||||
)
|
||||
if err != nil {
|
||||
log.Printf("警告: 扫描消息行失败: %v", err)
|
||||
log.Err(err).Msg("扫描消息行失败")
|
||||
continue
|
||||
}
|
||||
msg.CompressContent = compressContent
|
||||
@@ -283,7 +342,7 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T
|
||||
|
||||
// 对所有消息按时间排序
|
||||
sort.Slice(totalMessages, func(i, j int) bool {
|
||||
return totalMessages[i].Sequence < totalMessages[j].Sequence
|
||||
return totalMessages[i].Seq < totalMessages[j].Seq
|
||||
})
|
||||
|
||||
// 处理分页
|
||||
@@ -318,7 +377,7 @@ func (ds *DataSource) getMessagesSingleFile(ctx context.Context, dbInfo MessageD
|
||||
}
|
||||
}
|
||||
query := fmt.Sprintf(`
|
||||
SELECT Sequence, CreateTime, TalkerId, StrTalker, IsSender,
|
||||
SELECT Sequence, CreateTime, StrTalker, IsSender,
|
||||
Type, SubType, StrContent, CompressContent, BytesExtra
|
||||
FROM MSG
|
||||
WHERE %s
|
||||
@@ -336,7 +395,7 @@ func (ds *DataSource) getMessagesSingleFile(ctx context.Context, dbInfo MessageD
|
||||
// 执行查询
|
||||
rows, err := ds.messageDbs[dbInfo.FilePath].QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询数据库 %s 失败: %w", dbInfo.FilePath, err)
|
||||
return nil, errors.QueryFailed(query, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -349,7 +408,6 @@ func (ds *DataSource) getMessagesSingleFile(ctx context.Context, dbInfo MessageD
|
||||
err := rows.Scan(
|
||||
&msg.Sequence,
|
||||
&msg.CreateTime,
|
||||
&msg.TalkerID,
|
||||
&msg.StrTalker,
|
||||
&msg.IsSender,
|
||||
&msg.Type,
|
||||
@@ -359,7 +417,7 @@ func (ds *DataSource) getMessagesSingleFile(ctx context.Context, dbInfo MessageD
|
||||
&bytesExtra,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("扫描消息行失败: %w", err)
|
||||
return nil, errors.ScanRowFailed(err)
|
||||
}
|
||||
msg.CompressContent = compressContent
|
||||
msg.BytesExtra = bytesExtra
|
||||
@@ -395,7 +453,7 @@ func (ds *DataSource) GetContacts(ctx context.Context, key string, limit, offset
|
||||
// 执行查询
|
||||
rows, err := ds.contactDb.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询联系人失败: %w", err)
|
||||
return nil, errors.QueryFailed(query, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -411,7 +469,7 @@ func (ds *DataSource) GetContacts(ctx context.Context, key string, limit, offset
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("扫描联系人行失败: %w", err)
|
||||
return nil, errors.ScanRowFailed(err)
|
||||
}
|
||||
|
||||
contacts = append(contacts, contactV3.Wrap())
|
||||
@@ -433,7 +491,7 @@ func (ds *DataSource) GetChatRooms(ctx context.Context, key string, limit, offse
|
||||
// 执行查询
|
||||
rows, err := ds.contactDb.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询群聊失败: %w", err)
|
||||
return nil, errors.QueryFailed(query, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -447,7 +505,7 @@ func (ds *DataSource) GetChatRooms(ctx context.Context, key string, limit, offse
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("扫描群聊行失败: %w", err)
|
||||
return nil, errors.ScanRowFailed(err)
|
||||
}
|
||||
|
||||
chatRooms = append(chatRooms, chatRoomV3.Wrap())
|
||||
@@ -463,7 +521,7 @@ func (ds *DataSource) GetChatRooms(ctx context.Context, key string, limit, offse
|
||||
contacts[0].UserName)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询群聊失败: %w", err)
|
||||
return nil, errors.QueryFailed(query, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -476,7 +534,7 @@ func (ds *DataSource) GetChatRooms(ctx context.Context, key string, limit, offse
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("扫描群聊行失败: %w", err)
|
||||
return nil, errors.ScanRowFailed(err)
|
||||
}
|
||||
|
||||
chatRooms = append(chatRooms, chatRoomV3.Wrap())
|
||||
@@ -510,7 +568,7 @@ func (ds *DataSource) GetChatRooms(ctx context.Context, key string, limit, offse
|
||||
// 执行查询
|
||||
rows, err := ds.contactDb.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询群聊失败: %w", err)
|
||||
return nil, errors.QueryFailed(query, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -524,7 +582,7 @@ func (ds *DataSource) GetChatRooms(ctx context.Context, key string, limit, offse
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("扫描群聊行失败: %w", err)
|
||||
return nil, errors.ScanRowFailed(err)
|
||||
}
|
||||
|
||||
chatRooms = append(chatRooms, chatRoomV3.Wrap())
|
||||
@@ -564,7 +622,7 @@ func (ds *DataSource) GetSessions(ctx context.Context, key string, limit, offset
|
||||
// 执行查询
|
||||
rows, err := ds.contactDb.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("查询会话失败: %w", err)
|
||||
return nil, errors.QueryFailed(query, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
@@ -580,7 +638,7 @@ func (ds *DataSource) GetSessions(ctx context.Context, key string, limit, offset
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("扫描会话行失败: %w", err)
|
||||
return nil, errors.ScanRowFailed(err)
|
||||
}
|
||||
|
||||
sessions = append(sessions, sessionV3.Wrap())
|
||||
@@ -589,26 +647,120 @@ func (ds *DataSource) GetSessions(ctx context.Context, key string, limit, offset
|
||||
return sessions, nil
|
||||
}
|
||||
|
||||
func (ds *DataSource) GetMedia(ctx context.Context, _type string, key string) (*model.Media, error) {
|
||||
if key == "" {
|
||||
return nil, errors.ErrKeyEmpty
|
||||
}
|
||||
|
||||
md5key, err := hex.DecodeString(key)
|
||||
if err != nil {
|
||||
return nil, errors.DecodeKeyFailed(err)
|
||||
}
|
||||
|
||||
var db *sql.DB
|
||||
var table1, table2 string
|
||||
|
||||
switch _type {
|
||||
case "image":
|
||||
db = ds.imageDb
|
||||
table1 = "HardLinkImageAttribute"
|
||||
table2 = "HardLinkImageID"
|
||||
case "video":
|
||||
db = ds.videoDb
|
||||
table1 = "HardLinkVideoAttribute"
|
||||
table2 = "HardLinkVideoID"
|
||||
case "file":
|
||||
db = ds.fileDb
|
||||
table1 = "HardLinkFileAttribute"
|
||||
table2 = "HardLinkFileID"
|
||||
default:
|
||||
return nil, errors.MediaTypeUnsupported(_type)
|
||||
|
||||
}
|
||||
|
||||
query := fmt.Sprintf(`
|
||||
SELECT
|
||||
a.FileName,
|
||||
a.ModifyTime,
|
||||
IFNULL(d1.Dir,"") AS Dir1,
|
||||
IFNULL(d2.Dir,"") AS Dir2
|
||||
FROM
|
||||
%s a
|
||||
LEFT JOIN
|
||||
%s d1 ON a.DirID1 = d1.DirId
|
||||
LEFT JOIN
|
||||
%s d2 ON a.DirID2 = d2.DirId
|
||||
WHERE
|
||||
a.Md5 = ?
|
||||
`, table1, table2, table2)
|
||||
args := []interface{}{md5key}
|
||||
|
||||
rows, err := db.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return nil, errors.QueryFailed(query, err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var media *model.Media
|
||||
for rows.Next() {
|
||||
var mediaV3 model.MediaV3
|
||||
err := rows.Scan(
|
||||
&mediaV3.Name,
|
||||
&mediaV3.ModifyTime,
|
||||
&mediaV3.Dir1,
|
||||
&mediaV3.Dir2,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, errors.ScanRowFailed(err)
|
||||
}
|
||||
mediaV3.Type = _type
|
||||
mediaV3.Key = key
|
||||
media = mediaV3.Wrap()
|
||||
}
|
||||
|
||||
if media == nil {
|
||||
return nil, errors.ErrMediaNotFound
|
||||
}
|
||||
|
||||
return media, nil
|
||||
}
|
||||
|
||||
// Close 实现 DataSource 接口的 Close 方法
|
||||
func (ds *DataSource) Close() error {
|
||||
var errs []error
|
||||
|
||||
// 关闭消息数据库连接
|
||||
for path, db := range ds.messageDbs {
|
||||
for _, db := range ds.messageDbs {
|
||||
if err := db.Close(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("关闭消息数据库 %s 失败: %w", path, err))
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
// 关闭联系人数据库连接
|
||||
if ds.contactDb != nil {
|
||||
if err := ds.contactDb.Close(); err != nil {
|
||||
errs = append(errs, fmt.Errorf("关闭联系人数据库失败: %w", err))
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if ds.imageDb != nil {
|
||||
if err := ds.imageDb.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
if ds.videoDb != nil {
|
||||
if err := ds.videoDb.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
if ds.fileDb != nil {
|
||||
if err := ds.fileDb.Close(); err != nil {
|
||||
errs = append(errs, err)
|
||||
}
|
||||
}
|
||||
|
||||
if len(errs) > 0 {
|
||||
return fmt.Errorf("关闭数据库连接时发生错误: %v", errs)
|
||||
return errors.DBCloseFailed(errs[0])
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@@ -2,10 +2,10 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/errors"
|
||||
"github.com/sjzar/chatlog/internal/model"
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ func (r *Repository) initChatRoomCache(ctx context.Context) error {
|
||||
// 加载所有群聊到缓存
|
||||
chatRooms, err := r.ds.GetChatRooms(ctx, "", 0, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("加载群聊失败: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
chatRoomMap := make(map[string]*model.ChatRoom)
|
||||
@@ -75,7 +75,7 @@ func (r *Repository) GetChatRooms(ctx context.Context, key string, limit, offset
|
||||
if key != "" {
|
||||
ret = r.findChatRooms(key)
|
||||
if len(ret) == 0 {
|
||||
return nil, fmt.Errorf("未找到群聊: %s", key)
|
||||
return nil, errors.ChatRoomNotFound(key)
|
||||
}
|
||||
|
||||
if limit > 0 {
|
||||
@@ -111,7 +111,7 @@ func (r *Repository) GetChatRooms(ctx context.Context, key string, limit, offset
|
||||
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 nil, errors.ChatRoomNotFound(key)
|
||||
}
|
||||
return chatRoom, nil
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/errors"
|
||||
"github.com/sjzar/chatlog/internal/model"
|
||||
)
|
||||
|
||||
@@ -14,7 +14,7 @@ func (r *Repository) initContactCache(ctx context.Context) error {
|
||||
// 加载所有联系人到缓存
|
||||
contacts, err := r.ds.GetContacts(ctx, "", 0, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("加载联系人失败: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
contactMap := make(map[string]*model.Contact)
|
||||
@@ -78,7 +78,7 @@ func (r *Repository) GetContact(ctx context.Context, key string) (*model.Contact
|
||||
// 先尝试从缓存中获取
|
||||
contact := r.findContact(key)
|
||||
if contact == nil {
|
||||
return nil, fmt.Errorf("未找到联系人: %s", key)
|
||||
return nil, errors.ContactNotFound(key)
|
||||
}
|
||||
return contact, nil
|
||||
}
|
||||
@@ -88,7 +88,7 @@ func (r *Repository) GetContacts(ctx context.Context, key string, limit, offset
|
||||
if key != "" {
|
||||
ret = r.findContacts(key)
|
||||
if len(ret) == 0 {
|
||||
return nil, fmt.Errorf("未找到联系人: %s", key)
|
||||
return nil, errors.ContactNotFound(key)
|
||||
}
|
||||
if limit > 0 {
|
||||
end := offset + limit
|
||||
|
||||
11
internal/wechatdb/repository/media.go
Normal file
11
internal/wechatdb/repository/media.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/model"
|
||||
)
|
||||
|
||||
func (r *Repository) GetMedia(ctx context.Context, _type string, key string) (*model.Media, error) {
|
||||
return r.ds.GetMedia(ctx, _type, key)
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import (
|
||||
|
||||
"github.com/sjzar/chatlog/internal/model"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// GetMessages 实现 Repository 接口的 GetMessages 方法
|
||||
@@ -25,7 +25,7 @@ func (r *Repository) GetMessages(ctx context.Context, startTime, endTime time.Ti
|
||||
|
||||
// 补充消息信息
|
||||
if err := r.EnrichMessages(ctx, messages); err != nil {
|
||||
log.Debugf("EnrichMessages failed: %v", err)
|
||||
log.Debug().Msgf("EnrichMessages failed: %v", err)
|
||||
}
|
||||
|
||||
return messages, nil
|
||||
@@ -41,28 +41,24 @@ func (r *Repository) EnrichMessages(ctx context.Context, messages []*model.Messa
|
||||
|
||||
// enrichMessage 补充单条消息的额外信息
|
||||
func (r *Repository) enrichMessage(msg *model.Message) {
|
||||
talker := msg.Talker
|
||||
|
||||
// 处理群聊消息
|
||||
if msg.IsChatRoom {
|
||||
talker = msg.ChatRoomSender
|
||||
|
||||
// 补充群聊名称
|
||||
if chatRoom, ok := r.chatRoomCache[msg.Talker]; ok {
|
||||
msg.ChatRoomName = chatRoom.DisplayName()
|
||||
msg.TalkerName = chatRoom.DisplayName()
|
||||
|
||||
// 补充发送者在群里的显示名称
|
||||
if displayName, ok := chatRoom.User2DisplayName[talker]; ok {
|
||||
msg.DisplayName = displayName
|
||||
if displayName, ok := chatRoom.User2DisplayName[msg.Sender]; ok {
|
||||
msg.SenderName = displayName
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 如果不是自己发送的消息且还没有显示名称,尝试补充发送者信息
|
||||
if msg.DisplayName == "" && msg.IsSender != 1 {
|
||||
contact := r.getFullContact(talker)
|
||||
if msg.SenderName == "" && !msg.IsSelf {
|
||||
contact := r.getFullContact(msg.Sender)
|
||||
if contact != nil {
|
||||
msg.DisplayName = contact.DisplayName()
|
||||
msg.SenderName = contact.DisplayName()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,8 @@ package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/errors"
|
||||
"github.com/sjzar/chatlog/internal/model"
|
||||
"github.com/sjzar/chatlog/internal/wechatdb/datasource"
|
||||
)
|
||||
@@ -58,7 +58,7 @@ func New(ds datasource.DataSource) (*Repository, error) {
|
||||
|
||||
// 初始化缓存
|
||||
if err := r.initCache(context.Background()); err != nil {
|
||||
return nil, fmt.Errorf("初始化缓存失败: %w", err)
|
||||
return nil, errors.InitCacheFailed(err)
|
||||
}
|
||||
|
||||
return r, nil
|
||||
|
||||
@@ -2,7 +2,6 @@ package wechatdb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/sjzar/chatlog/internal/model"
|
||||
@@ -45,14 +44,14 @@ func (w *DB) Close() error {
|
||||
|
||||
func (w *DB) Initialize() error {
|
||||
var err error
|
||||
w.ds, err = datasource.NewDataSource(w.path, w.platform, w.version)
|
||||
w.ds, err = datasource.New(w.path, w.platform, w.version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("初始化数据源失败: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
w.repo, err = repository.New(w.ds)
|
||||
if err != nil {
|
||||
return fmt.Errorf("初始化仓库失败: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
@@ -64,7 +63,7 @@ func (w *DB) GetMessages(start, end time.Time, talker string, limit, offset int)
|
||||
// 使用 repository 获取消息
|
||||
messages, err := w.repo.GetMessages(ctx, start, end, talker, limit, offset)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取消息失败: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return messages, nil
|
||||
@@ -114,10 +113,14 @@ func (w *DB) GetSessions(key string, limit, offset int) (*GetSessionsResp, error
|
||||
// 使用 repository 获取会话列表
|
||||
sessions, err := w.repo.GetSessions(ctx, key, limit, offset)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("获取会话列表失败: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &GetSessionsResp{
|
||||
Items: sessions,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (w *DB) GetMedia(_type string, key string) (*model.Media, error) {
|
||||
return w.repo.GetMedia(context.Background(), _type, key)
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"errors"
|
||||
"os"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/rs/zerolog/log"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
@@ -141,7 +141,7 @@ func PrepareDir(path string) error {
|
||||
return err
|
||||
}
|
||||
} else if !stat.IsDir() {
|
||||
log.Debugf("%s is not a directory", path)
|
||||
log.Debug().Msgf("%s is not a directory", path)
|
||||
return ErrInvalidDirectory
|
||||
}
|
||||
return nil
|
||||
|
||||
302
pkg/util/dat2img/dat2img.go
Normal file
302
pkg/util/dat2img/dat2img.go
Normal file
@@ -0,0 +1,302 @@
|
||||
package dat2img
|
||||
|
||||
// Implementation based on:
|
||||
// - https://github.com/tujiaw/wechat_dat_to_image
|
||||
// - https://github.com/LC044/WeChatMsg/blob/6535ed0/wxManager/decrypt/decrypt_dat.py
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/aes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Format defines the header and extension for different image types
|
||||
type Format struct {
|
||||
Header []byte
|
||||
Ext string
|
||||
}
|
||||
|
||||
var (
|
||||
// Common image format definitions
|
||||
JPG = Format{Header: []byte{0xFF, 0xD8, 0xFF}, Ext: "jpg"}
|
||||
PNG = Format{Header: []byte{0x89, 0x50, 0x4E, 0x47}, Ext: "png"}
|
||||
GIF = Format{Header: []byte{0x47, 0x49, 0x46, 0x38}, Ext: "gif"}
|
||||
TIFF = Format{Header: []byte{0x49, 0x49, 0x2A, 0x00}, Ext: "tiff"}
|
||||
BMP = Format{Header: []byte{0x42, 0x4D}, Ext: "bmp"}
|
||||
Formats = []Format{JPG, PNG, GIF, TIFF, BMP}
|
||||
|
||||
// WeChat v4 related constants
|
||||
V4XorKey byte = 0x37 // Default XOR key for WeChat v4 dat files
|
||||
V4DatHeader = []byte{0x07, 0x08, 0x56, 0x31} // WeChat v4 dat file header
|
||||
JpgTail = []byte{0xFF, 0xD9} // JPG file tail marker
|
||||
)
|
||||
|
||||
// Dat2Image converts WeChat dat file data to image data
|
||||
// Returns the decoded image data, file extension, and any error encountered
|
||||
func Dat2Image(data []byte) ([]byte, string, error) {
|
||||
if len(data) < 4 {
|
||||
return nil, "", fmt.Errorf("data length is too short: %d", len(data))
|
||||
}
|
||||
|
||||
// Check if this is a WeChat v4 dat file
|
||||
if len(data) >= 6 && bytes.Equal(data[:4], V4DatHeader) {
|
||||
return Dat2ImageV4(data)
|
||||
}
|
||||
|
||||
// For older WeChat versions, use XOR decryption
|
||||
findFormat := func(data []byte, header []byte) bool {
|
||||
xorBit := data[0] ^ header[0]
|
||||
for i := 0; i < len(header); i++ {
|
||||
if data[i]^header[i] != xorBit {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
var xorBit byte
|
||||
var found bool
|
||||
var ext string
|
||||
for _, format := range Formats {
|
||||
if found = findFormat(data, format.Header); found {
|
||||
xorBit = data[0] ^ format.Header[0]
|
||||
ext = format.Ext
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return nil, "", fmt.Errorf("unknown image type: %x %x", data[0], data[1])
|
||||
}
|
||||
|
||||
// Apply XOR decryption
|
||||
out := make([]byte, len(data))
|
||||
for i := range data {
|
||||
out[i] = data[i] ^ xorBit
|
||||
}
|
||||
|
||||
return out, ext, nil
|
||||
}
|
||||
|
||||
// calculateXorKeyV4 calculates the XOR key for WeChat v4 dat files
|
||||
// by analyzing the file tail against known JPG ending bytes (FF D9)
|
||||
func calculateXorKeyV4(data []byte) (byte, error) {
|
||||
if len(data) < 2 {
|
||||
return 0, fmt.Errorf("data too short to calculate XOR key")
|
||||
}
|
||||
|
||||
// Get the last two bytes of the file
|
||||
fileTail := data[len(data)-2:]
|
||||
|
||||
// Assuming it's a JPG file, the tail should be FF D9
|
||||
xorKeys := make([]byte, 2)
|
||||
for i := 0; i < 2; i++ {
|
||||
xorKeys[i] = fileTail[i] ^ JpgTail[i]
|
||||
}
|
||||
|
||||
// Verify that both bytes yield the same XOR key
|
||||
if xorKeys[0] == xorKeys[1] {
|
||||
return xorKeys[0], nil
|
||||
}
|
||||
|
||||
// If inconsistent, return the first byte as key with a warning
|
||||
return xorKeys[0], fmt.Errorf("inconsistent XOR key, using first byte: 0x%x", xorKeys[0])
|
||||
}
|
||||
|
||||
// ScanAndSetXorKey scans a directory for "_t.dat" files to calculate and set
|
||||
// the global XOR key for WeChat v4 dat files
|
||||
// Returns the found key and any error encountered
|
||||
func ScanAndSetXorKey(dirPath string) (byte, error) {
|
||||
// Walk the directory recursively
|
||||
err := filepath.Walk(dirPath, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Skip directories
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Only process "_t.dat" files (thumbnail files)
|
||||
if !strings.HasSuffix(info.Name(), "_t.dat") {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Read file content
|
||||
data, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Check if it's a WeChat v4 dat file
|
||||
if len(data) < 6 || !bytes.Equal(data[:4], V4DatHeader) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Parse file header
|
||||
if len(data) < 15 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get XOR encryption length
|
||||
xorEncryptLen := binary.LittleEndian.Uint32(data[10:14])
|
||||
|
||||
// Get data after header
|
||||
fileData := data[15:]
|
||||
|
||||
// Skip if there's no XOR-encrypted part
|
||||
if xorEncryptLen == 0 || uint32(len(fileData)) <= uint32(len(fileData))-xorEncryptLen {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Get XOR-encrypted part
|
||||
xorData := fileData[uint32(len(fileData))-xorEncryptLen:]
|
||||
|
||||
// Calculate XOR key
|
||||
key, err := calculateXorKeyV4(xorData)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Set global XOR key
|
||||
V4XorKey = key
|
||||
|
||||
// Stop traversal after finding a valid key
|
||||
return filepath.SkipAll
|
||||
})
|
||||
|
||||
if err != nil && err != filepath.SkipAll {
|
||||
return V4XorKey, fmt.Errorf("error scanning directory: %v", err)
|
||||
}
|
||||
|
||||
return V4XorKey, nil
|
||||
}
|
||||
|
||||
// Dat2ImageV4 processes WeChat v4 dat image files
|
||||
// WeChat v4 uses a combination of AES-ECB and XOR encryption
|
||||
func Dat2ImageV4(data []byte) ([]byte, string, error) {
|
||||
if len(data) < 15 {
|
||||
return nil, "", fmt.Errorf("data length is too short for WeChat v4 format: %d", len(data))
|
||||
}
|
||||
|
||||
// Parse dat file header:
|
||||
// - 6 bytes: 0x07085631 (dat file identifier)
|
||||
// - 4 bytes: int (little-endian) AES-ECB128 encryption length
|
||||
// - 4 bytes: int (little-endian) XOR encryption length
|
||||
// - 1 byte: 0x01 (unknown)
|
||||
|
||||
// Read AES encryption length
|
||||
aesEncryptLen := binary.LittleEndian.Uint32(data[6:10])
|
||||
// Read XOR encryption length
|
||||
xorEncryptLen := binary.LittleEndian.Uint32(data[10:14])
|
||||
|
||||
// Data after header
|
||||
fileData := data[15:]
|
||||
|
||||
// AES encrypted part (max 1KB)
|
||||
// Round up to multiple of 16 bytes for AES block size
|
||||
aesEncryptLen0 := (aesEncryptLen)/16*16 + 16
|
||||
if aesEncryptLen0 > uint32(len(fileData)) {
|
||||
aesEncryptLen0 = uint32(len(fileData))
|
||||
}
|
||||
|
||||
// Decrypt AES part
|
||||
aesDecryptedData, err := decryptAESECB(fileData[:aesEncryptLen0], []byte("cfcd208495d565ef"))
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("AES decrypt error: %v", err)
|
||||
}
|
||||
|
||||
// Prepare result buffer
|
||||
var result []byte
|
||||
|
||||
// Add decrypted AES part (remove padding if necessary)
|
||||
if len(aesDecryptedData) > int(aesEncryptLen) {
|
||||
result = append(result, aesDecryptedData[:aesEncryptLen]...)
|
||||
} else {
|
||||
result = append(result, aesDecryptedData...)
|
||||
}
|
||||
|
||||
// Add unencrypted middle part
|
||||
middleStart := aesEncryptLen0
|
||||
middleEnd := uint32(len(fileData)) - xorEncryptLen
|
||||
if middleStart < middleEnd {
|
||||
result = append(result, fileData[middleStart:middleEnd]...)
|
||||
}
|
||||
|
||||
// Process XOR-encrypted part (file tail)
|
||||
if xorEncryptLen > 0 && middleEnd < uint32(len(fileData)) {
|
||||
xorData := fileData[middleEnd:]
|
||||
|
||||
// Apply XOR decryption using global key
|
||||
xorDecrypted := make([]byte, len(xorData))
|
||||
for i := range xorData {
|
||||
xorDecrypted[i] = xorData[i] ^ V4XorKey
|
||||
}
|
||||
|
||||
result = append(result, xorDecrypted...)
|
||||
}
|
||||
|
||||
// Identify image type from decrypted data
|
||||
imgType := ""
|
||||
for _, format := range Formats {
|
||||
if len(result) >= len(format.Header) && bytes.Equal(result[:len(format.Header)], format.Header) {
|
||||
imgType = format.Ext
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if imgType == "" {
|
||||
return nil, "", fmt.Errorf("unknown image type after decryption")
|
||||
}
|
||||
|
||||
return result, imgType, nil
|
||||
}
|
||||
|
||||
// decryptAESECB decrypts data using AES in ECB mode
|
||||
func decryptAESECB(data, key []byte) ([]byte, error) {
|
||||
if len(data) == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Create AES cipher
|
||||
cipher, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Ensure data length is a multiple of block size
|
||||
if len(data)%aes.BlockSize != 0 {
|
||||
return nil, fmt.Errorf("data length is not a multiple of block size")
|
||||
}
|
||||
|
||||
decrypted := make([]byte, len(data))
|
||||
|
||||
// ECB mode requires block-by-block decryption
|
||||
for bs, be := 0, aes.BlockSize; bs < len(data); bs, be = bs+aes.BlockSize, be+aes.BlockSize {
|
||||
cipher.Decrypt(decrypted[bs:be], data[bs:be])
|
||||
}
|
||||
|
||||
// Handle PKCS#7 padding
|
||||
padding := int(decrypted[len(decrypted)-1])
|
||||
if padding > 0 && padding <= aes.BlockSize {
|
||||
// Validate padding
|
||||
valid := true
|
||||
for i := len(decrypted) - padding; i < len(decrypted); i++ {
|
||||
if decrypted[i] != byte(padding) {
|
||||
valid = false
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if valid {
|
||||
return decrypted[:len(decrypted)-padding], nil
|
||||
}
|
||||
}
|
||||
|
||||
return decrypted, nil
|
||||
}
|
||||
16
pkg/util/lz4/lz4.go
Normal file
16
pkg/util/lz4/lz4.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package lz4
|
||||
|
||||
import (
|
||||
"github.com/pierrec/lz4/v4"
|
||||
)
|
||||
|
||||
func Decompress(src []byte) ([]byte, error) {
|
||||
// FIXME: lz4 的压缩率预计不到 3,这里设置了 4 保险一点
|
||||
out := make([]byte, len(src)*4)
|
||||
|
||||
n, err := lz4.UncompressBlock(src, out)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out[:n], nil
|
||||
}
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"regexp"
|
||||
"runtime"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/rs/zerolog/log"
|
||||
)
|
||||
|
||||
// FindFilesWithPatterns 在指定目录下查找匹配多个正则表达式的文件
|
||||
@@ -128,7 +128,7 @@ func PrepareDir(path string) error {
|
||||
return err
|
||||
}
|
||||
} else if !stat.IsDir() {
|
||||
log.Debugf("%s is not a directory", path)
|
||||
log.Debug().Msgf("%s is not a directory", path)
|
||||
return fmt.Errorf("%s is not a directory", path)
|
||||
}
|
||||
return nil
|
||||
|
||||
@@ -32,3 +32,16 @@ func MustAnyToInt(v interface{}) int {
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func IsNumeric(s string) bool {
|
||||
for _, r := range s {
|
||||
if !unicode.IsDigit(r) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return len(s) > 0
|
||||
}
|
||||
|
||||
func SplitInt64ToTwoInt32(input int64) (int64, int64) {
|
||||
return input & 0xFFFFFFFF, input >> 32
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user