Compare commits
1 Commits
v0.0.10
...
feature/da
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
39151258bb |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -27,5 +27,6 @@ go.work.sum
|
|||||||
# syncthing files
|
# syncthing files
|
||||||
.stfolder
|
.stfolder
|
||||||
|
|
||||||
|
chatlog
|
||||||
chatlog.exe# Added by goreleaser init:
|
chatlog.exe# Added by goreleaser init:
|
||||||
dist/
|
dist/
|
||||||
|
|||||||
@@ -1,146 +0,0 @@
|
|||||||
package chatlog
|
|
||||||
|
|
||||||
import (
|
|
||||||
"archive/zip"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"runtime"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
"github.com/spf13/cobra"
|
|
||||||
|
|
||||||
"github.com/sjzar/chatlog/internal/wechat"
|
|
||||||
"github.com/sjzar/chatlog/internal/wechat/key/darwin/glance"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
rootCmd.AddCommand(dumpmemoryCmd)
|
|
||||||
}
|
|
||||||
|
|
||||||
var dumpmemoryCmd = &cobra.Command{
|
|
||||||
Use: "dumpmemory",
|
|
||||||
Short: "dump memory",
|
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
|
||||||
if runtime.GOOS != "darwin" {
|
|
||||||
log.Info().Msg("dump memory only support macOS")
|
|
||||||
}
|
|
||||||
|
|
||||||
session := time.Now().Format("20060102150405")
|
|
||||||
|
|
||||||
dir, err := os.Getwd()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("get current directory failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Info().Msgf("current directory: %s", dir)
|
|
||||||
|
|
||||||
// step 1. check pid
|
|
||||||
if err = wechat.Load(); err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("load wechat failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
accounts := wechat.GetAccounts()
|
|
||||||
if len(accounts) == 0 {
|
|
||||||
log.Fatal().Msg("no wechat account found")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Msgf("found %d wechat account", len(accounts))
|
|
||||||
for i, a := range accounts {
|
|
||||||
log.Info().Msgf("%d. %s %d %s", i, a.FullVersion, a.PID, a.DataDir)
|
|
||||||
}
|
|
||||||
|
|
||||||
// step 2. dump memory
|
|
||||||
account := accounts[0]
|
|
||||||
file := fmt.Sprintf("wechat_%s_%d_%s.bin", account.FullVersion, account.PID, session)
|
|
||||||
path := filepath.Join(dir, file)
|
|
||||||
log.Info().Msgf("dumping memory to %s", path)
|
|
||||||
|
|
||||||
g := glance.NewGlance(account.PID)
|
|
||||||
b, err := g.Read()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("read memory failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err = os.WriteFile(path, b, 0644); err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("write memory failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Msg("dump memory success")
|
|
||||||
|
|
||||||
// step 3. copy encrypted database file
|
|
||||||
dbFile := "db_storage/session/session.db"
|
|
||||||
if account.Version == 3 {
|
|
||||||
dbFile = "Session/session_new.db"
|
|
||||||
}
|
|
||||||
from := filepath.Join(account.DataDir, dbFile)
|
|
||||||
to := filepath.Join(dir, fmt.Sprintf("wechat_%s_%d_session.db", account.FullVersion, account.PID))
|
|
||||||
|
|
||||||
log.Info().Msgf("copying %s to %s", from, to)
|
|
||||||
b, err = os.ReadFile(from)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("read session.db failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if err = os.WriteFile(to, b, 0644); err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("write session.db failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Info().Msg("copy session.db success")
|
|
||||||
|
|
||||||
// step 4. package
|
|
||||||
zipFile := fmt.Sprintf("wechat_%s_%d_%s.zip", account.FullVersion, account.PID, session)
|
|
||||||
zipPath := filepath.Join(dir, zipFile)
|
|
||||||
log.Info().Msgf("packaging to %s", zipPath)
|
|
||||||
|
|
||||||
zf, err := os.Create(zipPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("create zip file failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer zf.Close()
|
|
||||||
|
|
||||||
zw := zip.NewWriter(zf)
|
|
||||||
|
|
||||||
for _, file := range []string{file, to} {
|
|
||||||
f, err := os.Open(file)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("open file failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer f.Close()
|
|
||||||
info, err := f.Stat()
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("get file info failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
header, err := zip.FileInfoHeader(info)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("create zip file info header failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
header.Name = filepath.Base(file)
|
|
||||||
header.Method = zip.Deflate
|
|
||||||
writer, err := zw.CreateHeader(header)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("create zip file header failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, err = io.Copy(writer, f); err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("copy file to zip failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err = zw.Close(); err != nil {
|
|
||||||
log.Fatal().Err(err).Msg("close zip writer failed")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Info().Msgf("package success, please send %s to developer", zipPath)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
@@ -22,8 +22,6 @@ func initLog(cmd *cobra.Command, args []string) {
|
|||||||
if Debug {
|
if Debug {
|
||||||
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
zerolog.SetGlobalLevel(zerolog.DebugLevel)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr, TimeFormat: time.RFC3339})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func initTuiLog(cmd *cobra.Command, args []string) {
|
func initTuiLog(cmd *cobra.Command, args []string) {
|
||||||
|
|||||||
30
go.mod
30
go.mod
@@ -7,18 +7,16 @@ require (
|
|||||||
github.com/gin-gonic/gin v1.10.0
|
github.com/gin-gonic/gin v1.10.0
|
||||||
github.com/google/uuid v1.6.0
|
github.com/google/uuid v1.6.0
|
||||||
github.com/klauspost/compress v1.18.0
|
github.com/klauspost/compress v1.18.0
|
||||||
github.com/mattn/go-sqlite3 v1.14.27
|
github.com/mattn/go-sqlite3 v1.14.24
|
||||||
github.com/pierrec/lz4/v4 v4.1.22
|
github.com/pierrec/lz4/v4 v4.1.22
|
||||||
github.com/rivo/tview v0.0.0-20250330220935-949945f8d922
|
github.com/rivo/tview v0.0.0-20250325173046-7b72abf45814
|
||||||
github.com/rs/zerolog v1.34.0
|
github.com/rs/zerolog v1.34.0
|
||||||
github.com/shirou/gopsutil/v4 v4.25.3
|
github.com/shirou/gopsutil/v4 v4.25.2
|
||||||
github.com/sirupsen/logrus v1.9.3
|
github.com/sirupsen/logrus v1.9.3
|
||||||
github.com/sjzar/go-lame v0.0.8
|
|
||||||
github.com/sjzar/go-silk v0.0.1
|
|
||||||
github.com/spf13/cobra v1.9.1
|
github.com/spf13/cobra v1.9.1
|
||||||
github.com/spf13/viper v1.20.1
|
github.com/spf13/viper v1.20.1
|
||||||
golang.org/x/crypto v0.37.0
|
golang.org/x/crypto v0.36.0
|
||||||
golang.org/x/sys v0.32.0
|
golang.org/x/sys v0.31.0
|
||||||
google.golang.org/protobuf v1.36.6
|
google.golang.org/protobuf v1.36.6
|
||||||
howett.net/plist v1.0.1
|
howett.net/plist v1.0.1
|
||||||
)
|
)
|
||||||
@@ -28,14 +26,14 @@ require (
|
|||||||
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
github.com/bytedance/sonic/loader v0.2.4 // indirect
|
||||||
github.com/cloudwego/base64x v0.1.5 // indirect
|
github.com/cloudwego/base64x v0.1.5 // indirect
|
||||||
github.com/ebitengine/purego v0.8.2 // indirect
|
github.com/ebitengine/purego v0.8.2 // indirect
|
||||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
github.com/fsnotify/fsnotify v1.8.0 // indirect
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
github.com/gabriel-vasile/mimetype v1.4.8 // indirect
|
||||||
github.com/gdamore/encoding v1.0.1 // indirect
|
github.com/gdamore/encoding v1.0.1 // indirect
|
||||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
github.com/gin-contrib/sse v1.0.0 // indirect
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
github.com/go-playground/locales v0.14.1 // indirect
|
github.com/go-playground/locales v0.14.1 // indirect
|
||||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||||
github.com/go-playground/validator/v10 v10.26.0 // indirect
|
github.com/go-playground/validator/v10 v10.25.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
github.com/go-viper/mapstructure/v2 v2.2.1 // indirect
|
||||||
github.com/goccy/go-json v0.10.5 // indirect
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
@@ -44,12 +42,12 @@ require (
|
|||||||
github.com/leodido/go-urn v1.4.0 // indirect
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
||||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
github.com/mattn/go-runewidth v0.0.16 // indirect
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
github.com/rivo/uniseg v0.4.7 // indirect
|
||||||
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
github.com/sagikazarmark/locafero v0.9.0 // indirect
|
||||||
@@ -64,9 +62,9 @@ require (
|
|||||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
golang.org/x/arch v0.16.0 // indirect
|
golang.org/x/arch v0.15.0 // indirect
|
||||||
golang.org/x/net v0.39.0 // indirect
|
golang.org/x/net v0.38.0 // indirect
|
||||||
golang.org/x/term v0.31.0 // indirect
|
golang.org/x/term v0.30.0 // indirect
|
||||||
golang.org/x/text v0.24.0 // indirect
|
golang.org/x/text v0.23.0 // indirect
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
)
|
)
|
||||||
|
|||||||
61
go.sum
61
go.sum
@@ -15,16 +15,16 @@ github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z
|
|||||||
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M=
|
||||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM=
|
||||||
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8=
|
||||||
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
|
github.com/gdamore/encoding v1.0.1 h1:YzKZckdBL6jVt2Gc+5p82qhrGiqMdG/eNs6Wy0u3Uhw=
|
||||||
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
|
github.com/gdamore/encoding v1.0.1/go.mod h1:0Z0cMFinngz9kS1QfMjCP8TY7em3bZYeeklsSDPivEo=
|
||||||
github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU=
|
github.com/gdamore/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU=
|
||||||
github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw=
|
github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw=
|
||||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E=
|
||||||
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0=
|
||||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
@@ -36,8 +36,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
|||||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||||
github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k=
|
github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8=
|
||||||
github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus=
|
||||||
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
github.com/go-viper/mapstructure/v2 v2.2.1 h1:ZAaOCxANMuZx5RCeg0mBdEZk7DZasvvZIxtHqx8aGss=
|
||||||
github.com/go-viper/mapstructure/v2 v2.2.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM=
|
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 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
@@ -70,24 +70,23 @@ 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/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 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
|
||||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
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-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
|
||||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
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.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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mattn/go-sqlite3 v1.14.27 h1:drZCnuvf37yPfs95E5jd9s3XhdVWLal+6BOK6qrv6IU=
|
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||||
github.com/mattn/go-sqlite3 v1.14.27/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
|
||||||
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
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 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU=
|
||||||
github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
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/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
@@ -95,8 +94,8 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
github.com/rivo/tview v0.0.0-20250330220935-949945f8d922 h1:SMyqkaRfpE8ZQUSRTZKO3uN84xov++OGa+e3NCksaQw=
|
github.com/rivo/tview v0.0.0-20250325173046-7b72abf45814 h1:pJIO3sp+rkDbJTeqqpe2Oihq3hegiM5ASvsd6S0pvjg=
|
||||||
github.com/rivo/tview v0.0.0-20250330220935-949945f8d922/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss=
|
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.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.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||||
@@ -109,14 +108,10 @@ github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6
|
|||||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||||
github.com/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k=
|
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/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk=
|
||||||
github.com/shirou/gopsutil/v4 v4.25.3 h1:SeA68lsu8gLggyMbmCn8cmp97V1TI9ld9sVzAUcKcKE=
|
github.com/shirou/gopsutil/v4 v4.25.2 h1:NMscG3l2CqtWFS86kj3vP7soOczqrQYIEhO/pMvvQkk=
|
||||||
github.com/shirou/gopsutil/v4 v4.25.3/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA=
|
github.com/shirou/gopsutil/v4 v4.25.2/go.mod h1:34gBYJzyqCDT11b6bMHP0XCvWeU3J61XRT7a2EmCRTA=
|
||||||
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
|
||||||
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
|
||||||
github.com/sjzar/go-lame v0.0.8 h1:AS9l32R6foMiMEXWfUY8i79WIMfDoBC2QqQ9s5yziIk=
|
|
||||||
github.com/sjzar/go-lame v0.0.8/go.mod h1:8RmqWcAKSbBAk6bTRV9d8mdDxqK3hY9vFyoJ4DoQE6Y=
|
|
||||||
github.com/sjzar/go-silk v0.0.1 h1:cXD9dsIZti3n+g0Fd3IUvLH9A7tyL4jvUsHEyhff21s=
|
|
||||||
github.com/sjzar/go-silk v0.0.1/go.mod h1:IXVcHEXKiU9j3ZtHEiGS37OFKkex9pdAhZVcFzAIOlM=
|
|
||||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||||
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA=
|
||||||
@@ -132,11 +127,13 @@ github.com/spf13/viper v1.20.1/go.mod h1:P9Mdzt1zoHIG8m2eZQinpiBjo6kCmZSKBClNNqj
|
|||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
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.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||||
@@ -154,15 +151,15 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo
|
|||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||||
golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U=
|
golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw=
|
||||||
golang.org/x/arch v0.16.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE=
|
||||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
golang.org/x/crypto v0.13.0/go.mod h1:y6Z2r+Rw4iayiXXAIxJIDAJ1zMW4yaTpebo8fPOliYc=
|
||||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||||
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8=
|
||||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc=
|
||||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||||
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
golang.org/x/mod v0.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
|
||||||
@@ -176,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.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
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.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM=
|
||||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
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-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.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
@@ -202,8 +199,8 @@ golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|||||||
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.20.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
golang.org/x/sys v0.29.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
golang.org/x/telemetry v0.0.0-20240228155512-f48c80bd79b2/go.mod h1:TeRTkGYfJXctD9OcfyVLyj2J3IxLnKwHJR8f4D8a3YE=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
@@ -213,8 +210,8 @@ golang.org/x/term v0.12.0/go.mod h1:owVbMEjm3cBLCHdkQu9b1opXd4ETQWc3BhuQGKgXgvU=
|
|||||||
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk=
|
||||||
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
golang.org/x/term v0.20.0/go.mod h1:8UkIAJTvZgivsXaD6/pH6U9ecQzZ45awqEOzuCvwpFY=
|
||||||
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
golang.org/x/term v0.28.0/go.mod h1:Sw/lC2IAUZ92udQNf3WodGtn4k/XoLyZoh8v/8uiwek=
|
||||||
golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o=
|
golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y=
|
||||||
golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw=
|
golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
@@ -224,8 +221,8 @@ golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
|||||||
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
|
||||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY=
|
||||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||||
|
|||||||
@@ -74,16 +74,6 @@ func (c *Context) SwitchHistory(account string) {
|
|||||||
c.WorkDir = history.WorkDir
|
c.WorkDir = history.WorkDir
|
||||||
c.HTTPEnabled = history.HTTPEnabled
|
c.HTTPEnabled = history.HTTPEnabled
|
||||||
c.HTTPAddr = history.HTTPAddr
|
c.HTTPAddr = history.HTTPAddr
|
||||||
} else {
|
|
||||||
c.Account = ""
|
|
||||||
c.Platform = ""
|
|
||||||
c.Version = 0
|
|
||||||
c.FullVersion = ""
|
|
||||||
c.DataKey = ""
|
|
||||||
c.DataDir = ""
|
|
||||||
c.WorkDir = ""
|
|
||||||
c.HTTPEnabled = false
|
|
||||||
c.HTTPAddr = ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import (
|
|||||||
"github.com/sjzar/chatlog/internal/errors"
|
"github.com/sjzar/chatlog/internal/errors"
|
||||||
"github.com/sjzar/chatlog/pkg/util"
|
"github.com/sjzar/chatlog/pkg/util"
|
||||||
"github.com/sjzar/chatlog/pkg/util/dat2img"
|
"github.com/sjzar/chatlog/pkg/util/dat2img"
|
||||||
"github.com/sjzar/chatlog/pkg/util/silk"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
@@ -35,7 +34,6 @@ func (s *Service) initRouter() {
|
|||||||
router.GET("/image/:key", s.GetImage)
|
router.GET("/image/:key", s.GetImage)
|
||||||
router.GET("/video/:key", s.GetVideo)
|
router.GET("/video/:key", s.GetVideo)
|
||||||
router.GET("/file/:key", s.GetFile)
|
router.GET("/file/:key", s.GetFile)
|
||||||
router.GET("/voice/:key", s.GetVoice)
|
|
||||||
router.GET("/data/*path", s.GetMediaData)
|
router.GET("/data/*path", s.GetMediaData)
|
||||||
|
|
||||||
// MCP Server
|
// MCP Server
|
||||||
@@ -274,9 +272,6 @@ func (s *Service) GetVideo(c *gin.Context) {
|
|||||||
func (s *Service) GetFile(c *gin.Context) {
|
func (s *Service) GetFile(c *gin.Context) {
|
||||||
s.GetMedia(c, "file")
|
s.GetMedia(c, "file")
|
||||||
}
|
}
|
||||||
func (s *Service) GetVoice(c *gin.Context) {
|
|
||||||
s.GetMedia(c, "voice")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Service) GetMedia(c *gin.Context, _type string) {
|
func (s *Service) GetMedia(c *gin.Context, _type string) {
|
||||||
key := c.Param("key")
|
key := c.Param("key")
|
||||||
@@ -296,13 +291,7 @@ func (s *Service) GetMedia(c *gin.Context, _type string) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
switch media.Type {
|
|
||||||
case "voice":
|
|
||||||
s.HandleVoice(c, media.Data)
|
|
||||||
default:
|
|
||||||
c.Redirect(http.StatusFound, "/data/"+media.Path)
|
c.Redirect(http.StatusFound, "/data/"+media.Path)
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) GetMediaData(c *gin.Context) {
|
func (s *Service) GetMediaData(c *gin.Context) {
|
||||||
@@ -354,12 +343,3 @@ func (s *Service) HandleDatFile(c *gin.Context, path string) {
|
|||||||
c.File(path)
|
c.File(path)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) HandleVoice(c *gin.Context, data []byte) {
|
|
||||||
out, err := silk.Silk2MP3(data)
|
|
||||||
if err != nil {
|
|
||||||
c.Data(http.StatusOK, "audio/silk", data)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
c.Data(http.StatusOK, "audio/mp3", out)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ var (
|
|||||||
"description": "联系人的搜索关键词,可以是姓名、备注名或ID。",
|
"description": "联系人的搜索关键词,可以是姓名、备注名或ID。",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Required: []string{"query"},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,7 +40,6 @@ var (
|
|||||||
"description": "群聊的搜索关键词,可以是群名称、群ID或相关描述",
|
"description": "群聊的搜索关键词,可以是群名称、群ID或相关描述",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Required: []string{"query"},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -69,7 +67,6 @@ var (
|
|||||||
"description": "交谈对象,可以是联系人或群聊。支持使用ID、昵称、备注名等进行查询。",
|
"description": "交谈对象,可以是联系人或群聊。支持使用ID、昵称、备注名等进行查询。",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Required: []string{"time", "talker"},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -191,6 +191,9 @@ func (s *Service) toolsCall(session *mcp.Session, req *mcp.Request) error {
|
|||||||
talker = v.(string)
|
talker = v.(string)
|
||||||
}
|
}
|
||||||
limit := util.MustAnyToInt(callReq.Arguments["limit"])
|
limit := util.MustAnyToInt(callReq.Arguments["limit"])
|
||||||
|
if limit == 0 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
offset := util.MustAnyToInt(callReq.Arguments["offset"])
|
offset := util.MustAnyToInt(callReq.Arguments["offset"])
|
||||||
messages, err := s.db.GetMessages(start, end, talker, limit, offset)
|
messages, err := s.db.GetMessages(start, end, talker, limit, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -261,6 +264,9 @@ func (s *Service) resourcesRead(session *mcp.Session, req *mcp.Request) error {
|
|||||||
return fmt.Errorf("无法解析时间范围")
|
return fmt.Errorf("无法解析时间范围")
|
||||||
}
|
}
|
||||||
limit := util.MustAnyToInt(u.Query().Get("limit"))
|
limit := util.MustAnyToInt(u.Query().Get("limit"))
|
||||||
|
if limit == 0 {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
offset := util.MustAnyToInt(u.Query().Get("offset"))
|
offset := util.MustAnyToInt(u.Query().Get("offset"))
|
||||||
messages, err := s.db.GetMessages(start, end, u.Host, limit, offset)
|
messages, err := s.db.GetMessages(start, end, u.Host, limit, offset)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ func RootCause(err error) error {
|
|||||||
|
|
||||||
func Err(c *gin.Context, err error) {
|
func Err(c *gin.Context, err error) {
|
||||||
if appErr, ok := err.(*Error); ok {
|
if appErr, ok := err.(*Error); ok {
|
||||||
c.JSON(appErr.Code, appErr.Error())
|
c.JSON(appErr.Code, appErr)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -94,7 +94,6 @@ type Tool struct {
|
|||||||
type ToolSchema struct {
|
type ToolSchema struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Properties M `json:"properties"`
|
Properties M `json:"properties"`
|
||||||
Required []string `json:"required,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// {
|
// {
|
||||||
|
|||||||
@@ -20,17 +20,9 @@ func (c *ChatRoomV4) Wrap() *ChatRoom {
|
|||||||
users = ParseRoomData(c.ExtBuffer)
|
users = ParseRoomData(c.ExtBuffer)
|
||||||
}
|
}
|
||||||
|
|
||||||
user2DisplayName := make(map[string]string, len(users))
|
|
||||||
for _, user := range users {
|
|
||||||
if user.DisplayName != "" {
|
|
||||||
user2DisplayName[user.UserName] = user.DisplayName
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &ChatRoom{
|
return &ChatRoom{
|
||||||
Name: c.UserName,
|
Name: c.UserName,
|
||||||
Owner: c.Owner,
|
Owner: c.Owner,
|
||||||
Users: users,
|
Users: users,
|
||||||
User2DisplayName: user2DisplayName,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,33 @@ type ContactV3 struct {
|
|||||||
Remark string `json:"Remark"`
|
Remark string `json:"Remark"`
|
||||||
NickName string `json:"NickName"`
|
NickName string `json:"NickName"`
|
||||||
Reserved1 int `json:"Reserved1"` // 1 自己好友或自己加入的群聊; 0 群聊成员(非好友)
|
Reserved1 int `json:"Reserved1"` // 1 自己好友或自己加入的群聊; 0 群聊成员(非好友)
|
||||||
|
|
||||||
|
// EncryptUserName string `json:"EncryptUserName"`
|
||||||
|
// DelFlag int `json:"DelFlag"`
|
||||||
|
// Type int `json:"Type"`
|
||||||
|
// VerifyFlag int `json:"VerifyFlag"`
|
||||||
|
// Reserved2 int `json:"Reserved2"`
|
||||||
|
// Reserved3 string `json:"Reserved3"`
|
||||||
|
// Reserved4 string `json:"Reserved4"`
|
||||||
|
// LabelIDList string `json:"LabelIDList"`
|
||||||
|
// DomainList string `json:"DomainList"`
|
||||||
|
// ChatRoomType int `json:"ChatRoomType"`
|
||||||
|
// PYInitial string `json:"PYInitial"`
|
||||||
|
// QuanPin string `json:"QuanPin"`
|
||||||
|
// RemarkPYInitial string `json:"RemarkPYInitial"`
|
||||||
|
// RemarkQuanPin string `json:"RemarkQuanPin"`
|
||||||
|
// BigHeadImgUrl string `json:"BigHeadImgUrl"`
|
||||||
|
// SmallHeadImgUrl string `json:"SmallHeadImgUrl"`
|
||||||
|
// HeadImgMd5 string `json:"HeadImgMd5"`
|
||||||
|
// ChatRoomNotify int `json:"ChatRoomNotify"`
|
||||||
|
// Reserved5 int `json:"Reserved5"`
|
||||||
|
// Reserved6 string `json:"Reserved6"`
|
||||||
|
// Reserved7 string `json:"Reserved7"`
|
||||||
|
// ExtraBuf []byte `json:"ExtraBuf"`
|
||||||
|
// Reserved8 int `json:"Reserved8"`
|
||||||
|
// Reserved9 int `json:"Reserved9"`
|
||||||
|
// Reserved10 string `json:"Reserved10"`
|
||||||
|
// Reserved11 string `json:"Reserved11"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ContactV3) Wrap() *Contact {
|
func (c *ContactV3) Wrap() *Contact {
|
||||||
|
|||||||
@@ -40,6 +40,33 @@ type ContactDarwinV3 struct {
|
|||||||
M_nsRemark string `json:"m_nsRemark"`
|
M_nsRemark string `json:"m_nsRemark"`
|
||||||
M_uiSex int `json:"m_uiSex"`
|
M_uiSex int `json:"m_uiSex"`
|
||||||
M_nsAliasName string `json:"m_nsAliasName"`
|
M_nsAliasName string `json:"m_nsAliasName"`
|
||||||
|
|
||||||
|
// M_uiConType int `json:"m_uiConType"`
|
||||||
|
// M_nsShortPY string `json:"m_nsShortPY"`
|
||||||
|
// M_nsRemarkPYFull string `json:"m_nsRemarkPYFull"`
|
||||||
|
// M_nsRemarkPYShort string `json:"m_nsRemarkPYShort"`
|
||||||
|
// M_uiCertificationFlag int `json:"m_uiCertificationFlag"`
|
||||||
|
// M_uiType int `json:"m_uiType"` // 本来想拿这个字段来区分是否是好友,但是数据比较乱,好在 darwin v3 Contact 表中没有群聊成员
|
||||||
|
// M_nsImgStatus string `json:"m_nsImgStatus"`
|
||||||
|
// M_uiImgKey int `json:"m_uiImgKey"`
|
||||||
|
// M_nsHeadImgUrl string `json:"m_nsHeadImgUrl"`
|
||||||
|
// M_nsHeadHDImgUrl string `json:"m_nsHeadHDImgUrl"`
|
||||||
|
// M_nsHeadHDMd5 string `json:"m_nsHeadHDMd5"`
|
||||||
|
// M_nsChatRoomMemList string `json:"m_nsChatRoomMemList"`
|
||||||
|
// M_nsChatRoomAdminList string `json:"m_nsChatRoomAdminList"`
|
||||||
|
// M_uiChatRoomStatus int `json:"m_uiChatRoomStatus"`
|
||||||
|
// M_nsChatRoomDesc string `json:"m_nsChatRoomDesc"`
|
||||||
|
// M_nsDraft string `json:"m_nsDraft"`
|
||||||
|
// M_nsBrandIconUrl string `json:"m_nsBrandIconUrl"`
|
||||||
|
// M_nsGoogleContactName string `json:"m_nsGoogleContactName"`
|
||||||
|
// M_nsEncodeUserName string `json:"m_nsEncodeUserName"`
|
||||||
|
// M_uiChatRoomVersion int `json:"m_uiChatRoomVersion"`
|
||||||
|
// M_uiChatRoomMaxCount int `json:"m_uiChatRoomMaxCount"`
|
||||||
|
// M_uiChatRoomType int `json:"m_uiChatRoomType"`
|
||||||
|
// M_patSuffix string `json:"m_patSuffix"`
|
||||||
|
// RichChatRoomDesc string `json:"richChatRoomDesc"`
|
||||||
|
// Packed_WCContactData string `json:"_packed_WCContactData"`
|
||||||
|
// OpenIMInfo string `json:"openIMInfo"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ContactDarwinV3) Wrap() *Contact {
|
func (c *ContactDarwinV3) Wrap() *Contact {
|
||||||
|
|||||||
@@ -30,6 +30,25 @@ type ContactV4 struct {
|
|||||||
Remark string `json:"remark"`
|
Remark string `json:"remark"`
|
||||||
NickName string `json:"nick_name"`
|
NickName string `json:"nick_name"`
|
||||||
LocalType int `json:"local_type"` // 2 群聊; 3 群聊成员(非好友); 5,6 企业微信;
|
LocalType int `json:"local_type"` // 2 群聊; 3 群聊成员(非好友); 5,6 企业微信;
|
||||||
|
|
||||||
|
// ID int `json:"id"`
|
||||||
|
|
||||||
|
// EncryptUserName string `json:"encrypt_username"`
|
||||||
|
// Flag int `json:"flag"`
|
||||||
|
// DeleteFlag int `json:"delete_flag"`
|
||||||
|
// VerifyFlag int `json:"verify_flag"`
|
||||||
|
// RemarkQuanPin string `json:"remark_quan_pin"`
|
||||||
|
// RemarkPinYinInitial string `json:"remark_pin_yin_initial"`
|
||||||
|
// PinYinInitial string `json:"pin_yin_initial"`
|
||||||
|
// QuanPin string `json:"quan_pin"`
|
||||||
|
// BigHeadUrl string `json:"big_head_url"`
|
||||||
|
// SmallHeadUrl string `json:"small_head_url"`
|
||||||
|
// HeadImgMd5 string `json:"head_img_md5"`
|
||||||
|
// ChatRoomNotify int `json:"chat_room_notify"`
|
||||||
|
// IsInChatRoom int `json:"is_in_chat_room"`
|
||||||
|
// Description string `json:"description"`
|
||||||
|
// ExtraBuffer []byte `json:"extra_buffer"`
|
||||||
|
// ChatRoomType int `json:"chat_room_type"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ContactV4) Wrap() *Contact {
|
func (c *ContactV4) Wrap() *Contact {
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ type Media struct {
|
|||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Size int64 `json:"size"`
|
Size int64 `json:"size"`
|
||||||
Data []byte `json:"data"` // for voice
|
|
||||||
ModifyTime int64 `json:"modifyTime"`
|
ModifyTime int64 `json:"modifyTime"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,17 +3,217 @@ package model
|
|||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"encoding/xml"
|
||||||
"fmt"
|
"fmt"
|
||||||
"regexp"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/sjzar/chatlog/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type MediaMsg struct {
|
type MediaMessage struct {
|
||||||
|
Type int64
|
||||||
|
SubType int
|
||||||
|
MediaMD5 string
|
||||||
|
MediaPath string
|
||||||
|
Title string
|
||||||
|
Desc string
|
||||||
|
Content string
|
||||||
|
URL string
|
||||||
|
|
||||||
|
RecordInfo *RecordInfo
|
||||||
|
|
||||||
|
ReferDisplayName string
|
||||||
|
ReferUserName string
|
||||||
|
ReferCreateTime time.Time
|
||||||
|
ReferMessage *MediaMessage
|
||||||
|
|
||||||
|
Host string
|
||||||
|
|
||||||
|
Message XMLMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMediaMessage(_type int64, data string) (*MediaMessage, error) {
|
||||||
|
|
||||||
|
__type, subType := util.SplitInt64ToTwoInt32(_type)
|
||||||
|
|
||||||
|
m := &MediaMessage{
|
||||||
|
Type: __type,
|
||||||
|
SubType: int(subType),
|
||||||
|
}
|
||||||
|
|
||||||
|
if _type == 1 {
|
||||||
|
m.Content = data
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var msg XMLMessage
|
||||||
|
err := xml.Unmarshal([]byte(data), &msg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
m.Message = msg
|
||||||
|
if err := m.parse(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MediaMessage) parse() error {
|
||||||
|
|
||||||
|
switch m.Type {
|
||||||
|
case 3:
|
||||||
|
m.MediaMD5 = m.Message.Image.MD5
|
||||||
|
case 43:
|
||||||
|
m.MediaMD5 = m.Message.Video.RawMd5
|
||||||
|
case 49:
|
||||||
|
m.SubType = m.Message.App.Type
|
||||||
|
switch m.SubType {
|
||||||
|
case 5:
|
||||||
|
m.Title = m.Message.App.Title
|
||||||
|
m.URL = m.Message.App.URL
|
||||||
|
case 6:
|
||||||
|
m.Title = m.Message.App.Title
|
||||||
|
m.MediaMD5 = m.Message.App.MD5
|
||||||
|
case 19:
|
||||||
|
m.Title = m.Message.App.Title
|
||||||
|
m.Desc = m.Message.App.Des
|
||||||
|
if m.Message.App.RecordItem == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
recordInfo := &RecordInfo{}
|
||||||
|
err := xml.Unmarshal([]byte(m.Message.App.RecordItem.CDATA), recordInfo)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
m.RecordInfo = recordInfo
|
||||||
|
case 57:
|
||||||
|
m.Content = m.Message.App.Title
|
||||||
|
if m.Message.App.ReferMsg == nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
subMsg, err := NewMediaMessage(m.Message.App.ReferMsg.Type, m.Message.App.ReferMsg.Content)
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
m.ReferDisplayName = m.Message.App.ReferMsg.DisplayName
|
||||||
|
m.ReferUserName = m.Message.App.ReferMsg.ChatUsr
|
||||||
|
m.ReferCreateTime = time.Unix(m.Message.App.ReferMsg.CreateTime, 0)
|
||||||
|
m.ReferMessage = subMsg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MediaMessage) SetHost(host string) {
|
||||||
|
m.Host = host
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MediaMessage) String() string {
|
||||||
|
switch m.Type {
|
||||||
|
case 1:
|
||||||
|
return m.Content
|
||||||
|
case 3:
|
||||||
|
return fmt.Sprintf("", m.Host, m.MediaMD5)
|
||||||
|
case 34:
|
||||||
|
return "[语音]"
|
||||||
|
case 43:
|
||||||
|
if m.MediaPath != "" {
|
||||||
|
return fmt.Sprintf("", m.Host, m.MediaPath)
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("", m.Host, m.MediaMD5)
|
||||||
|
case 47:
|
||||||
|
return "[动画表情]"
|
||||||
|
case 49:
|
||||||
|
switch m.SubType {
|
||||||
|
case 5:
|
||||||
|
return fmt.Sprintf("[链接|%s](%s)", m.Title, m.URL)
|
||||||
|
case 6:
|
||||||
|
return fmt.Sprintf("[文件|%s](http://%s/file/%s)", m.Title, m.Host, m.MediaMD5)
|
||||||
|
case 8:
|
||||||
|
return "[GIF表情]"
|
||||||
|
case 19:
|
||||||
|
if m.RecordInfo == nil {
|
||||||
|
return "[合并转发]"
|
||||||
|
}
|
||||||
|
buf := strings.Builder{}
|
||||||
|
for _, item := range m.RecordInfo.DataList.DataItems {
|
||||||
|
buf.WriteString(item.SourceName + ": ")
|
||||||
|
switch item.DataType {
|
||||||
|
case "jpg":
|
||||||
|
buf.WriteString(fmt.Sprintf("", m.Host, item.FullMD5))
|
||||||
|
default:
|
||||||
|
buf.WriteString(item.DataDesc)
|
||||||
|
}
|
||||||
|
buf.WriteString("\n")
|
||||||
|
}
|
||||||
|
return m.Content
|
||||||
|
case 33, 36:
|
||||||
|
return "[小程序]"
|
||||||
|
case 57:
|
||||||
|
if m.ReferMessage == nil {
|
||||||
|
if m.Content == "" {
|
||||||
|
return "[引用]"
|
||||||
|
}
|
||||||
|
return "> [引用]\n" + m.Content
|
||||||
|
}
|
||||||
|
buf := strings.Builder{}
|
||||||
|
buf.WriteString("> ")
|
||||||
|
if m.ReferDisplayName != "" {
|
||||||
|
buf.WriteString(m.ReferDisplayName)
|
||||||
|
buf.WriteString("(")
|
||||||
|
buf.WriteString(m.ReferUserName)
|
||||||
|
buf.WriteString(")")
|
||||||
|
} else {
|
||||||
|
buf.WriteString(m.ReferUserName)
|
||||||
|
}
|
||||||
|
buf.WriteString(" ")
|
||||||
|
buf.WriteString(m.ReferCreateTime.Format("2006-01-02 15:04:05"))
|
||||||
|
buf.WriteString("\n")
|
||||||
|
buf.WriteString("> ")
|
||||||
|
m.ReferMessage.SetHost(m.Host)
|
||||||
|
buf.WriteString(strings.ReplaceAll(m.ReferMessage.String(), "\n", "\n> "))
|
||||||
|
buf.WriteString("\n")
|
||||||
|
buf.WriteString(m.Content)
|
||||||
|
m.Content = buf.String()
|
||||||
|
return m.Content
|
||||||
|
case 63:
|
||||||
|
return "[视频号]"
|
||||||
|
case 87:
|
||||||
|
return "[群公告]"
|
||||||
|
case 2000:
|
||||||
|
return "[转账]"
|
||||||
|
case 2003:
|
||||||
|
return "[红包封面]"
|
||||||
|
default:
|
||||||
|
return "[分享]"
|
||||||
|
}
|
||||||
|
case 50:
|
||||||
|
return "[语音通话]"
|
||||||
|
case 10000:
|
||||||
|
return "[系统消息]"
|
||||||
|
default:
|
||||||
|
content := m.Content
|
||||||
|
if len(content) > 120 {
|
||||||
|
content = content[:120] + "<...>"
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("Type: %d Content: %s", m.Type, content)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type XMLMessage struct {
|
||||||
XMLName xml.Name `xml:"msg"`
|
XMLName xml.Name `xml:"msg"`
|
||||||
Image Image `xml:"img,omitempty"`
|
Image Image `xml:"img,omitempty"`
|
||||||
Video Video `xml:"videomsg,omitempty"`
|
Video Video `xml:"videomsg,omitempty"`
|
||||||
App App `xml:"appmsg,omitempty"`
|
App App `xml:"appmsg,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type XMLImageMessage struct {
|
||||||
|
XMLName xml.Name `xml:"msg"`
|
||||||
|
Img Image `xml:"img"`
|
||||||
|
}
|
||||||
|
|
||||||
type Image struct {
|
type Image struct {
|
||||||
MD5 string `xml:"md5,attr"`
|
MD5 string `xml:"md5,attr"`
|
||||||
// HdLength string `xml:"hdlength,attr"`
|
// HdLength string `xml:"hdlength,attr"`
|
||||||
@@ -34,6 +234,11 @@ type Image struct {
|
|||||||
// CdnThumbAesKey string `xml:"cdnthumbaeskey,attr"`
|
// CdnThumbAesKey string `xml:"cdnthumbaeskey,attr"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type XMLVideoMessage struct {
|
||||||
|
XMLName xml.Name `xml:"msg"`
|
||||||
|
VideoMsg Video `xml:"videomsg"`
|
||||||
|
}
|
||||||
|
|
||||||
type Video struct {
|
type Video struct {
|
||||||
RawMd5 string `xml:"rawmd5,attr"`
|
RawMd5 string `xml:"rawmd5,attr"`
|
||||||
// Length string `xml:"length,attr"`
|
// Length string `xml:"length,attr"`
|
||||||
@@ -62,14 +267,10 @@ type App struct {
|
|||||||
Title string `xml:"title"`
|
Title string `xml:"title"`
|
||||||
Des string `xml:"des"`
|
Des string `xml:"des"`
|
||||||
URL string `xml:"url"` // type 5 分享
|
URL string `xml:"url"` // type 5 分享
|
||||||
AppAttach *AppAttach `xml:"appattach,omitempty"` // type 6 文件
|
AppAttach AppAttach `xml:"appattach"` // type 6 文件
|
||||||
MD5 string `xml:"md5,omitempty"` // type 6 文件
|
MD5 string `xml:"md5"` // type 6 文件
|
||||||
RecordItem *RecordItem `xml:"recorditem,omitempty"` // type 19 合并转发
|
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 引用
|
ReferMsg *ReferMsg `xml:"refermsg,omitempty"` // type 57 引用
|
||||||
PatMsg *PatMsg `xml:"patMsg,omitempty"` // type 62 拍一拍
|
|
||||||
WCPayInfo *WCPayInfo `xml:"wcpayinfo,omitempty"` // type 2000 微信转账
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ReferMsg 表示引用消息
|
// ReferMsg 表示引用消息
|
||||||
@@ -151,273 +352,4 @@ type DataItem struct {
|
|||||||
SrcMsgCreateTime string `xml:"srcMsgCreateTime,omitempty"`
|
SrcMsgCreateTime string `xml:"srcMsgCreateTime,omitempty"`
|
||||||
MessageUUID string `xml:"messageuuid,omitempty"`
|
MessageUUID string `xml:"messageuuid,omitempty"`
|
||||||
FromNewMsgID string `xml:"fromnewmsgid,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 {
|
|
||||||
Type string `xml:"type,attr"`
|
|
||||||
DelChatRoomMember *DelChatRoomMember `xml:"delchatroommember,omitempty"`
|
|
||||||
SysMsgTemplate *SysMsgTemplate `xml:"sysmsgtemplate,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 第一种消息类型:删除群成员/二维码邀请
|
|
||||||
type DelChatRoomMember struct {
|
|
||||||
Plain string `xml:"plain"`
|
|
||||||
Text string `xml:"text"`
|
|
||||||
Link QRLink `xml:"link"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type QRLink struct {
|
|
||||||
Scene string `xml:"scene"`
|
|
||||||
Text string `xml:"text"`
|
|
||||||
MemberList QRMemberList `xml:"memberlist"`
|
|
||||||
QRCode string `xml:"qrcode"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type QRMemberList struct {
|
|
||||||
Usernames []UsernameItem `xml:"username"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UsernameItem struct {
|
|
||||||
Value string `xml:",chardata"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// 第二种消息类型:系统消息模板
|
|
||||||
type SysMsgTemplate struct {
|
|
||||||
ContentTemplate ContentTemplate `xml:"content_template"`
|
|
||||||
}
|
|
||||||
|
|
||||||
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,omitempty"`
|
|
||||||
Title string `xml:"title,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MemberList struct {
|
|
||||||
Members []Member `xml:"member"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Member struct {
|
|
||||||
Username string `xml:"username"`
|
|
||||||
Nickname string `xml:"nickname"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SysMsg) String() string {
|
|
||||||
if s.Type == "delchatroommember" {
|
|
||||||
return s.DelChatRoomMemberString()
|
|
||||||
}
|
|
||||||
return s.SysMsgTemplateString()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SysMsg) DelChatRoomMemberString() string {
|
|
||||||
if s.DelChatRoomMember == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return s.DelChatRoomMember.Plain
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *SysMsg) SysMsgTemplateString() string {
|
|
||||||
if s.SysMsgTemplate == nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
template := s.SysMsgTemplate.ContentTemplate.Template
|
|
||||||
links := s.SysMsgTemplate.ContentTemplate.LinkList.Links
|
|
||||||
|
|
||||||
// 创建一个映射,用于存储占位符名称和对应的替换内容
|
|
||||||
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:
|
|
||||||
if link.Title != "" {
|
|
||||||
replacement = link.Title
|
|
||||||
} else {
|
|
||||||
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,212 +1,197 @@
|
|||||||
package model
|
package model
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/xml"
|
"path/filepath"
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/sjzar/chatlog/pkg/util"
|
"github.com/sjzar/chatlog/internal/model/wxproto"
|
||||||
|
"github.com/sjzar/chatlog/pkg/util/lz4"
|
||||||
|
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
)
|
)
|
||||||
|
|
||||||
var Debug = false
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// Source
|
||||||
WeChatV3 = "wechatv3"
|
WeChatV3 = "wechatv3"
|
||||||
WeChatV4 = "wechatv4"
|
WeChatV4 = "wechatv4"
|
||||||
WeChatDarwinV3 = "wechatdarwinv3"
|
WeChatDarwinV3 = "wechatdarwinv3"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Message struct {
|
type Message struct {
|
||||||
Version string `json:"-"` // 消息版本,内部判断
|
Sequence int64 `json:"sequence"` // 消息序号,10位时间戳 + 3位序号
|
||||||
Seq int64 `json:"seq"` // 消息序号,10位时间戳 + 3位序号
|
CreateTime time.Time `json:"createTime"` // 消息创建时间,10位时间戳
|
||||||
Time time.Time `json:"time"` // 消息创建时间,10位时间戳
|
TalkerID int `json:"talkerID"` // 聊天对象,Name2ID 表序号,索引值
|
||||||
Talker string `json:"talker"` // 聊天对象,微信 ID or 群 ID
|
Talker string `json:"talker"` // 聊天对象,微信 ID or 群 ID
|
||||||
TalkerName string `json:"talkerName"` // 聊天对象名称
|
IsSender int `json:"isSender"` // 是否为发送消息,0 接收消息,1 发送消息
|
||||||
IsChatRoom bool `json:"isChatRoom"` // 是否为群聊消息
|
|
||||||
Sender string `json:"sender"` // 发送人,微信 ID
|
|
||||||
SenderName string `json:"senderName"` // 发送人名称
|
|
||||||
IsSelf bool `json:"isSelf"` // 是否为自己发送的消息
|
|
||||||
Type int64 `json:"type"` // 消息类型
|
Type int64 `json:"type"` // 消息类型
|
||||||
SubType int64 `json:"subType"` // 消息子类型
|
SubType int `json:"subType"` // 消息子类型
|
||||||
Content string `json:"content"` // 消息内容,文字聊天内容
|
Content string `json:"content"` // 消息内容,文字聊天内容 或 XML
|
||||||
Contents map[string]interface{} `json:"contents,omitempty"` // 消息内容,多媒体消息,采用更灵活的记录方式
|
CompressContent []byte `json:"compressContent"` // 非文字聊天内容,如图片、语音、视频等
|
||||||
|
IsChatRoom bool `json:"isChatRoom"` // 是否为群聊消息
|
||||||
|
ChatRoomSender string `json:"chatRoomSender"` // 群聊消息发送人
|
||||||
|
|
||||||
// Debug Info
|
// Fill Info
|
||||||
MediaMsg *MediaMsg `json:"mediaMsg,omitempty"` // 原始多媒体消息,XML 格式
|
// 从联系人等信息中填充
|
||||||
SysMsg *SysMsg `json:"sysMsg,omitempty"` // 原始系统消息,XML 格式
|
DisplayName string `json:"-"` // 显示名称
|
||||||
|
ChatRoomName string `json:"-"` // 群聊名称
|
||||||
|
MediaMessage *MediaMessage `json:"-"` // 多媒体消息
|
||||||
|
|
||||||
|
Version string `json:"-"` // 消息版本,内部判断
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Message) ParseMediaInfo(data string) error {
|
// 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 int64 `json:"Type"` // 消息类型
|
||||||
|
SubType int `json:"SubType"` // 消息子类型
|
||||||
|
StrContent string `json:"StrContent"` // 消息内容,文字聊天内容 或 XML
|
||||||
|
CompressContent []byte `json:"CompressContent"` // 非文字聊天内容,如图片、语音、视频等
|
||||||
|
BytesExtra []byte `json:"BytesExtra"` // protobuf 额外数据,记录群聊发送人等信息
|
||||||
|
|
||||||
m.Type, m.SubType = util.SplitInt64ToTwoInt32(m.Type)
|
// 非关键信息,后续有需要再加入
|
||||||
|
// LocalID int64 `json:"localId"`
|
||||||
if m.Type == 1 {
|
// MsgSvrID int64 `json:"MsgSvrID"`
|
||||||
m.Content = data
|
// StatusEx int `json:"StatusEx"`
|
||||||
return nil
|
// FlagEx int `json:"FlagEx"`
|
||||||
}
|
// Status int `json:"Status"`
|
||||||
|
// MsgServerSeq int64 `json:"MsgServerSeq"`
|
||||||
if m.Type == 10000 {
|
// MsgSequence int64 `json:"MsgSequence"`
|
||||||
var sysMsg SysMsg
|
// DisplayContent string `json:"DisplayContent"`
|
||||||
if err := xml.Unmarshal([]byte(data), &sysMsg); err != nil {
|
// Reserved0 int `json:"Reserved0"`
|
||||||
m.Content = data
|
// Reserved1 int `json:"Reserved1"`
|
||||||
return nil
|
// Reserved2 int `json:"Reserved2"`
|
||||||
}
|
// Reserved3 int `json:"Reserved3"`
|
||||||
if Debug {
|
// Reserved4 string `json:"Reserved4"`
|
||||||
m.SysMsg = &sysMsg
|
// Reserved5 string `json:"Reserved5"`
|
||||||
}
|
// Reserved6 string `json:"Reserved6"`
|
||||||
m.Sender = "系统消息"
|
// BytesTrans []byte `json:"BytesTrans"`
|
||||||
m.SenderName = ""
|
|
||||||
m.Content = sysMsg.String()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Message) SetContent(key string, value interface{}) {
|
func (m *MessageV3) Wrap() *Message {
|
||||||
if m.Contents == nil {
|
|
||||||
m.Contents = make(map[string]interface{})
|
_m := &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,
|
||||||
|
Version: WeChatV3,
|
||||||
}
|
}
|
||||||
m.Contents[key] = value
|
|
||||||
|
_m.IsChatRoom = strings.HasSuffix(_m.Talker, "@chatroom")
|
||||||
|
|
||||||
|
if _m.Type == 49 {
|
||||||
|
b, err := lz4.Decompress(m.CompressContent)
|
||||||
|
if err == nil {
|
||||||
|
_m.Content = string(b)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if _m.Type != 1 {
|
||||||
|
mediaMessage, err := NewMediaMessage(_m.Type, _m.Content)
|
||||||
|
if err == nil {
|
||||||
|
_m.MediaMessage = mediaMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(m.BytesExtra) != 0 {
|
||||||
|
if bytesExtra := ParseBytesExtra(m.BytesExtra); bytesExtra != nil {
|
||||||
|
if _m.IsChatRoom {
|
||||||
|
_m.ChatRoomSender = bytesExtra[1]
|
||||||
|
}
|
||||||
|
// FIXME xml 中的 md5 数据无法匹配到 hardlink 记录,所以直接用 proto 数据
|
||||||
|
if _m.Type == 43 {
|
||||||
|
path := bytesExtra[4]
|
||||||
|
parts := strings.Split(filepath.ToSlash(path), "/")
|
||||||
|
if len(parts) > 1 {
|
||||||
|
path = strings.Join(parts[1:], "/")
|
||||||
|
}
|
||||||
|
_m.MediaMessage.MediaPath = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return _m
|
||||||
|
}
|
||||||
|
|
||||||
|
// ParseBytesExtra 解析额外数据
|
||||||
|
// 按需解析
|
||||||
|
func ParseBytesExtra(b []byte) 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
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Message) PlainText(showChatRoom bool, host string) string {
|
func (m *Message) PlainText(showChatRoom bool, host string) string {
|
||||||
|
|
||||||
m.SetContent("host", host)
|
|
||||||
|
|
||||||
buf := strings.Builder{}
|
buf := strings.Builder{}
|
||||||
|
|
||||||
sender := m.Sender
|
talker := m.Talker
|
||||||
if m.IsSelf {
|
if m.IsSender == 1 {
|
||||||
sender = "我"
|
talker = "我"
|
||||||
|
} else if m.IsChatRoom {
|
||||||
|
talker = m.ChatRoomSender
|
||||||
}
|
}
|
||||||
if m.SenderName != "" {
|
if m.DisplayName != "" {
|
||||||
buf.WriteString(m.SenderName)
|
buf.WriteString(m.DisplayName)
|
||||||
buf.WriteString("(")
|
buf.WriteString("(")
|
||||||
buf.WriteString(sender)
|
buf.WriteString(talker)
|
||||||
buf.WriteString(")")
|
buf.WriteString(")")
|
||||||
} else {
|
} else {
|
||||||
buf.WriteString(sender)
|
buf.WriteString(talker)
|
||||||
}
|
}
|
||||||
buf.WriteString(" ")
|
buf.WriteString(" ")
|
||||||
|
|
||||||
if m.IsChatRoom && showChatRoom {
|
if m.IsChatRoom && showChatRoom {
|
||||||
buf.WriteString("[")
|
buf.WriteString("[")
|
||||||
if m.TalkerName != "" {
|
if m.ChatRoomName != "" {
|
||||||
buf.WriteString(m.TalkerName)
|
buf.WriteString(m.ChatRoomName)
|
||||||
buf.WriteString("(")
|
buf.WriteString("(")
|
||||||
buf.WriteString(m.Talker)
|
buf.WriteString(m.Talker)
|
||||||
buf.WriteString(")")
|
buf.WriteString(")")
|
||||||
@@ -216,115 +201,17 @@ func (m *Message) PlainText(showChatRoom bool, host string) string {
|
|||||||
buf.WriteString("] ")
|
buf.WriteString("] ")
|
||||||
}
|
}
|
||||||
|
|
||||||
buf.WriteString(m.Time.Format("2006-01-02 15:04:05"))
|
buf.WriteString(m.CreateTime.Format("2006-01-02 15:04:05"))
|
||||||
buf.WriteString("\n")
|
buf.WriteString("\n")
|
||||||
|
|
||||||
buf.WriteString(m.PlainTextContent())
|
if m.MediaMessage != nil {
|
||||||
buf.WriteString("\n")
|
m.MediaMessage.SetHost(host)
|
||||||
|
buf.WriteString(m.MediaMessage.String())
|
||||||
return buf.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *Message) PlainTextContent() string {
|
|
||||||
switch m.Type {
|
|
||||||
case 1:
|
|
||||||
return m.Content
|
|
||||||
case 3:
|
|
||||||
return fmt.Sprintf("", m.Contents["host"], m.Contents["md5"])
|
|
||||||
case 34:
|
|
||||||
if voice, ok := m.Contents["voice"]; ok {
|
|
||||||
return fmt.Sprintf("[语音](http://%s/voice/%s)", m.Contents["host"], voice)
|
|
||||||
}
|
|
||||||
return "[语音]"
|
|
||||||
case 42:
|
|
||||||
return "[名片]"
|
|
||||||
case 43:
|
|
||||||
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:
|
|
||||||
return "[动画表情]"
|
|
||||||
case 49:
|
|
||||||
switch m.SubType {
|
|
||||||
case 5:
|
|
||||||
return fmt.Sprintf("[链接|%s](%s)", m.Contents["title"], m.Contents["url"])
|
|
||||||
case 6:
|
|
||||||
return fmt.Sprintf("[文件|%s](http://%s/file/%s)", m.Contents["title"], m.Contents["host"], m.Contents["md5"])
|
|
||||||
case 8:
|
|
||||||
return "[GIF表情]"
|
|
||||||
case 19:
|
|
||||||
_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:
|
|
||||||
if m.Contents["title"] == "" {
|
|
||||||
return "[小程序]"
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("[小程序|%s](%s)", m.Contents["title"], m.Contents["url"])
|
|
||||||
case 51:
|
|
||||||
if m.Contents["title"] == "" {
|
|
||||||
return "[视频号]"
|
|
||||||
} else {
|
} else {
|
||||||
return fmt.Sprintf("[视频号|%s](%s)", m.Contents["title"], m.Contents["url"])
|
|
||||||
}
|
|
||||||
case 57:
|
|
||||||
_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)
|
buf.WriteString(m.Content)
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.WriteString("\n")
|
||||||
|
|
||||||
return buf.String()
|
return buf.String()
|
||||||
case 62:
|
|
||||||
return m.Content
|
|
||||||
case 63:
|
|
||||||
return "[视频号]"
|
|
||||||
case 87:
|
|
||||||
return "[群公告]"
|
|
||||||
case 2000:
|
|
||||||
return m.Content
|
|
||||||
case 2001:
|
|
||||||
return "[红包]"
|
|
||||||
case 2003:
|
|
||||||
return "[红包封面]"
|
|
||||||
default:
|
|
||||||
return "[分享]"
|
|
||||||
}
|
|
||||||
case 50:
|
|
||||||
return "[语音通话]"
|
|
||||||
case 10000:
|
|
||||||
return m.Content
|
|
||||||
default:
|
|
||||||
content := m.Content
|
|
||||||
if len(content) > 120 {
|
|
||||||
content = content[:120] + "<...>"
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("Type: %d Content: %s", m.Type, content)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,31 +27,48 @@ type MessageDarwinV3 struct {
|
|||||||
MsgContent string `json:"msgContent"`
|
MsgContent string `json:"msgContent"`
|
||||||
MessageType int64 `json:"messageType"`
|
MessageType int64 `json:"messageType"`
|
||||||
MesDes int `json:"mesDes"` // 0: 发送, 1: 接收
|
MesDes int `json:"mesDes"` // 0: 发送, 1: 接收
|
||||||
|
|
||||||
|
// MesLocalID int64 `json:"mesLocalID"`
|
||||||
|
// MesSvrID int64 `json:"mesSvrID"`
|
||||||
|
// MesStatus int `json:"mesStatus"`
|
||||||
|
// MesImgStatus int `json:"mesImgStatus"`
|
||||||
|
// MsgSource string `json:"msgSource"`
|
||||||
|
// IntRes1 int `json:"IntRes1"`
|
||||||
|
// IntRes2 int `json:"IntRes2"`
|
||||||
|
// StrRes1 string `json:"StrRes1"`
|
||||||
|
// 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 {
|
func (m *MessageDarwinV3) Wrap(talker string) *Message {
|
||||||
|
|
||||||
_m := &Message{
|
_m := &Message{
|
||||||
Time: time.Unix(m.MsgCreateTime, 0),
|
CreateTime: time.Unix(m.MsgCreateTime, 0),
|
||||||
Type: m.MessageType,
|
Type: m.MessageType,
|
||||||
Talker: talker,
|
IsSender: (m.MesDes + 1) % 2,
|
||||||
IsChatRoom: strings.HasSuffix(talker, "@chatroom"),
|
|
||||||
IsSelf: m.MesDes == 0,
|
|
||||||
Version: WeChatDarwinV3,
|
Version: WeChatDarwinV3,
|
||||||
}
|
}
|
||||||
|
|
||||||
content := m.MsgContent
|
_m.IsChatRoom = strings.HasSuffix(talker, "@chatroom")
|
||||||
|
|
||||||
|
_m.Content = m.MsgContent
|
||||||
if _m.IsChatRoom {
|
if _m.IsChatRoom {
|
||||||
split := strings.SplitN(content, ":\n", 2)
|
split := strings.SplitN(m.MsgContent, ":\n", 2)
|
||||||
if len(split) == 2 {
|
if len(split) == 2 {
|
||||||
_m.Sender = split[0]
|
_m.ChatRoomSender = split[0]
|
||||||
content = split[1]
|
_m.Content = split[1]
|
||||||
}
|
}
|
||||||
} else if !_m.IsSelf {
|
|
||||||
_m.Sender = talker
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_m.ParseMediaInfo(content)
|
if _m.Type != 1 {
|
||||||
|
mediaMessage, err := NewMediaMessage(_m.Type, _m.Content)
|
||||||
|
if err == nil {
|
||||||
|
_m.MediaMessage = mediaMessage
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return _m
|
return _m
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,124 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/sjzar/chatlog/internal/model/wxproto"
|
|
||||||
"github.com/sjzar/chatlog/pkg/util/lz4"
|
|
||||||
"google.golang.org/protobuf/proto"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 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 {
|
|
||||||
MsgSvrID int64 `json:"MsgSvrID"` // 消息 ID
|
|
||||||
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 _m.Type == 34 {
|
|
||||||
_m.Contents["voice"] = fmt.Sprint(m.MsgSvrID)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(m.BytesExtra) != 0 {
|
|
||||||
if bytesExtra := ParseBytesExtra(m.BytesExtra); bytesExtra != nil {
|
|
||||||
if _m.IsChatRoom {
|
|
||||||
_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
|
|
||||||
}
|
|
||||||
@@ -2,7 +2,6 @@ package model
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -32,63 +31,76 @@ import (
|
|||||||
// )
|
// )
|
||||||
type MessageV4 struct {
|
type MessageV4 struct {
|
||||||
SortSeq int64 `json:"sort_seq"` // 消息序号,10位时间戳 + 3位序号
|
SortSeq int64 `json:"sort_seq"` // 消息序号,10位时间戳 + 3位序号
|
||||||
ServerID int64 `json:"server_id"` // 消息 ID,用于关联 voice
|
|
||||||
LocalType int64 `json:"local_type"` // 消息类型
|
LocalType int64 `json:"local_type"` // 消息类型
|
||||||
UserName string `json:"user_name"` // 发送人,通过 Join Name2Id 表获得
|
RealSenderID int `json:"real_sender_id"` // 发送人 ID,对应 Name2Id 表序号
|
||||||
CreateTime int64 `json:"create_time"` // 消息创建时间,10位时间戳
|
CreateTime int64 `json:"create_time"` // 消息创建时间,10位时间戳
|
||||||
MessageContent []byte `json:"message_content"` // 消息内容,文字聊天内容 或 zstd 压缩内容
|
MessageContent []byte `json:"message_content"` // 消息内容,文字聊天内容 或 zstd 压缩内容
|
||||||
PackedInfoData []byte `json:"packed_info_data"` // 额外数据,类似 proto,格式与 v3 有差异
|
PackedInfoData []byte `json:"packed_info_data"` // 额外数据,类似 proto,格式与 v3 有差异
|
||||||
Status int `json:"status"` // 消息状态,2 是已发送,4 是已接收,可以用于判断 IsSender(FIXME 不准, 需要判断 UserName)
|
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *MessageV4) Wrap(talker string) *Message {
|
func (m *MessageV4) Wrap(id2Name map[int]string, isChatRoom bool) *Message {
|
||||||
|
|
||||||
_m := &Message{
|
_m := &Message{
|
||||||
Seq: m.SortSeq,
|
Sequence: m.SortSeq,
|
||||||
Time: time.Unix(m.CreateTime, 0),
|
CreateTime: time.Unix(m.CreateTime, 0),
|
||||||
Talker: talker,
|
TalkerID: m.RealSenderID, // 依赖 Name2Id 表进行转换为 StrTalker
|
||||||
IsChatRoom: strings.HasSuffix(talker, "@chatroom"),
|
|
||||||
Sender: m.UserName,
|
|
||||||
Type: m.LocalType,
|
Type: m.LocalType,
|
||||||
Contents: make(map[string]interface{}),
|
|
||||||
Version: WeChatV4,
|
Version: WeChatV4,
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME 后续通过 UserName 判断是否是自己发送的消息,目前可能不准确
|
if name, ok := id2Name[m.RealSenderID]; ok {
|
||||||
_m.IsSelf = m.Status == 2 || (!_m.IsChatRoom && talker != m.UserName)
|
_m.Talker = name
|
||||||
|
}
|
||||||
|
|
||||||
|
if m.Status == 2 {
|
||||||
|
_m.IsSender = 1
|
||||||
|
}
|
||||||
|
|
||||||
content := ""
|
|
||||||
if bytes.HasPrefix(m.MessageContent, []byte{0x28, 0xb5, 0x2f, 0xfd}) {
|
if bytes.HasPrefix(m.MessageContent, []byte{0x28, 0xb5, 0x2f, 0xfd}) {
|
||||||
if b, err := zstd.Decompress(m.MessageContent); err == nil {
|
if b, err := zstd.Decompress(m.MessageContent); err == nil {
|
||||||
content = string(b)
|
_m.Content = string(b)
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
content = string(m.MessageContent)
|
_m.Content = string(m.MessageContent)
|
||||||
}
|
}
|
||||||
|
|
||||||
if _m.IsChatRoom {
|
if isChatRoom {
|
||||||
split := strings.SplitN(content, ":\n", 2)
|
_m.IsChatRoom = true
|
||||||
|
split := strings.SplitN(_m.Content, ":\n", 2)
|
||||||
if len(split) == 2 {
|
if len(split) == 2 {
|
||||||
_m.Sender = split[0]
|
_m.ChatRoomSender = split[0]
|
||||||
content = split[1]
|
_m.Content = split[1]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_m.ParseMediaInfo(content)
|
if _m.Type != 1 {
|
||||||
|
mediaMessage, err := NewMediaMessage(_m.Type, _m.Content)
|
||||||
// 语音消息
|
if err == nil {
|
||||||
if _m.Type == 34 {
|
_m.MediaMessage = mediaMessage
|
||||||
_m.Contents["voice"] = fmt.Sprint(m.ServerID)
|
_m.Type = mediaMessage.Type
|
||||||
|
_m.SubType = mediaMessage.SubType
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(m.PackedInfoData) != 0 {
|
if len(m.PackedInfoData) != 0 {
|
||||||
if packedInfo := ParsePackedInfo(m.PackedInfoData); packedInfo != nil {
|
if packedInfo := ParsePackedInfo(m.PackedInfoData); packedInfo != nil {
|
||||||
// FIXME 尝试解决 v4 版本 xml 数据无法匹配到 hardlink 记录的问题
|
// FIXME 尝试解决 v4 版本 xml 数据无法匹配到 hardlink 记录的问题
|
||||||
if _m.Type == 3 && packedInfo.Image != nil {
|
if _m.Type == 3 && packedInfo.Image != nil {
|
||||||
_m.Contents["md5"] = packedInfo.Image.Md5
|
_m.MediaMessage.MediaMD5 = packedInfo.Image.Md5
|
||||||
}
|
}
|
||||||
if _m.Type == 43 && packedInfo.Video != nil {
|
if _m.Type == 43 && packedInfo.Video != nil {
|
||||||
_m.Contents["md5"] = packedInfo.Video.Md5
|
_m.MediaMessage.MediaMD5 = packedInfo.Video.Md5
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,17 +15,13 @@ type Validator struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewValidator 创建一个仅用于验证的验证器
|
// NewValidator 创建一个仅用于验证的验证器
|
||||||
func NewValidator(platform string, version int, dataDir string) (*Validator, error) {
|
func NewValidator(dataDir string, platform string, version int) (*Validator, error) {
|
||||||
dbFile := GetSimpleDBFile(platform, version)
|
|
||||||
dbPath := filepath.Join(dataDir + "/" + dbFile)
|
|
||||||
return NewValidatorWithFile(platform, version, dbPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewValidatorWithFile(platform string, version int, dbPath string) (*Validator, error) {
|
|
||||||
decryptor, err := NewDecryptor(platform, version)
|
decryptor, err := NewDecryptor(platform, version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
dbFile := GetSimpleDBFile(platform, version)
|
||||||
|
dbPath := filepath.Join(dataDir + "/" + dbFile)
|
||||||
d, err := common.OpenDBFile(dbPath, decryptor.GetPageSize())
|
d, err := common.OpenDBFile(dbPath, decryptor.GetPageSize())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -19,22 +19,12 @@ const (
|
|||||||
MaxWorkersV3 = 8
|
MaxWorkersV3 = 8
|
||||||
)
|
)
|
||||||
|
|
||||||
var V3KeyPatterns = []KeyPatternInfo{
|
|
||||||
{
|
|
||||||
Pattern: []byte{0x72, 0x74, 0x72, 0x65, 0x65, 0x5f, 0x69, 0x33, 0x32},
|
|
||||||
Offset: 24,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
type V3Extractor struct {
|
type V3Extractor struct {
|
||||||
validator *decrypt.Validator
|
validator *decrypt.Validator
|
||||||
keyPatterns []KeyPatternInfo
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewV3Extractor() *V3Extractor {
|
func NewV3Extractor() *V3Extractor {
|
||||||
return &V3Extractor{
|
return &V3Extractor{}
|
||||||
keyPatterns: V3KeyPatterns,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *V3Extractor) Extract(ctx context.Context, proc *model.Process) (string, error) {
|
func (e *V3Extractor) Extract(ctx context.Context, proc *model.Process) (string, error) {
|
||||||
@@ -137,6 +127,7 @@ func (e *V3Extractor) findMemory(ctx context.Context, pid uint32, memoryChannel
|
|||||||
|
|
||||||
// worker processes memory regions to find V3 version key
|
// worker processes memory regions to find V3 version key
|
||||||
func (e *V3Extractor) worker(ctx context.Context, memoryChannel <-chan []byte, resultChannel chan<- string) {
|
func (e *V3Extractor) worker(ctx context.Context, memoryChannel <-chan []byte, resultChannel chan<- string) {
|
||||||
|
keyPattern := []byte{0x72, 0x74, 0x72, 0x65, 0x65, 0x5f, 0x69, 0x33, 0x32}
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@@ -146,58 +137,49 @@ func (e *V3Extractor) worker(ctx context.Context, memoryChannel <-chan []byte, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if key, ok := e.SearchKey(ctx, memory); ok {
|
|
||||||
select {
|
|
||||||
case resultChannel <- key:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *V3Extractor) SearchKey(ctx context.Context, memory []byte) (string, bool) {
|
|
||||||
for _, keyPattern := range e.keyPatterns {
|
|
||||||
index := len(memory)
|
index := len(memory)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return "", false
|
return // Exit if context cancelled
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debug().Msgf("Searching for V3 key in memory region, size: %d bytes", len(memory))
|
||||||
|
|
||||||
// Find pattern from end to beginning
|
// Find pattern from end to beginning
|
||||||
index = bytes.LastIndex(memory[:index], keyPattern.Pattern)
|
index = bytes.LastIndex(memory[:index], keyPattern)
|
||||||
if index == -1 {
|
if index == -1 {
|
||||||
break // No more matches found
|
break // No more matches found
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we have enough space for the key
|
log.Debug().Msgf("Found potential V3 key pattern in memory region, index: %d", index)
|
||||||
keyOffset := index + keyPattern.Offset
|
|
||||||
if keyOffset < 0 || keyOffset+32 > len(memory) {
|
// For V3, the key is 32 bytes and starts right after the pattern
|
||||||
|
if index+24+32 > len(memory) {
|
||||||
index -= 1
|
index -= 1
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the key data, which is 32 bytes long
|
// Extract the key data, which is right after the pattern and 32 bytes long
|
||||||
|
keyOffset := index + 24
|
||||||
keyData := memory[keyOffset : keyOffset+32]
|
keyData := memory[keyOffset : keyOffset+32]
|
||||||
|
|
||||||
// Validate key against database header
|
// Validate key against database header
|
||||||
if e.validator.Validate(keyData) {
|
if e.validator.Validate(keyData) {
|
||||||
log.Debug().
|
select {
|
||||||
Str("pattern", hex.EncodeToString(keyPattern.Pattern)).
|
case resultChannel <- hex.EncodeToString(keyData):
|
||||||
Int("offset", keyPattern.Offset).
|
log.Debug().Msg("Key found: " + hex.EncodeToString(keyData))
|
||||||
Str("key", hex.EncodeToString(keyData)).
|
return
|
||||||
Msg("Key found")
|
default:
|
||||||
return hex.EncodeToString(keyData), true
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
index -= 1
|
index -= 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return "", false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *V3Extractor) SetValidate(validator *decrypt.Validator) {
|
func (e *V3Extractor) SetValidate(validator *decrypt.Validator) {
|
||||||
|
|||||||
@@ -19,26 +19,12 @@ const (
|
|||||||
MaxWorkers = 8
|
MaxWorkers = 8
|
||||||
)
|
)
|
||||||
|
|
||||||
var V4KeyPatterns = []KeyPatternInfo{
|
|
||||||
{
|
|
||||||
Pattern: []byte{0x20, 0x66, 0x74, 0x73, 0x35, 0x28, 0x25, 0x00},
|
|
||||||
Offset: 16,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Pattern: []byte{0x20, 0x66, 0x74, 0x73, 0x35, 0x28, 0x25, 0x00},
|
|
||||||
Offset: -80,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
type V4Extractor struct {
|
type V4Extractor struct {
|
||||||
validator *decrypt.Validator
|
validator *decrypt.Validator
|
||||||
keyPatterns []KeyPatternInfo
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewV4Extractor() *V4Extractor {
|
func NewV4Extractor() *V4Extractor {
|
||||||
return &V4Extractor{
|
return &V4Extractor{}
|
||||||
keyPatterns: V4KeyPatterns,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *V4Extractor) Extract(ctx context.Context, proc *model.Process) (string, error) {
|
func (e *V4Extractor) Extract(ctx context.Context, proc *model.Process) (string, error) {
|
||||||
@@ -141,6 +127,8 @@ func (e *V4Extractor) findMemory(ctx context.Context, pid uint32, memoryChannel
|
|||||||
|
|
||||||
// worker processes memory regions to find V4 version key
|
// worker processes memory regions to find V4 version key
|
||||||
func (e *V4Extractor) worker(ctx context.Context, memoryChannel <-chan []byte, resultChannel chan<- string) {
|
func (e *V4Extractor) worker(ctx context.Context, memoryChannel <-chan []byte, resultChannel chan<- string) {
|
||||||
|
keyPattern := []byte{0x20, 0x66, 0x74, 0x73, 0x35, 0x28, 0x25, 0x00}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@@ -150,65 +138,47 @@ func (e *V4Extractor) worker(ctx context.Context, memoryChannel <-chan []byte, r
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if key, ok := e.SearchKey(ctx, memory); ok {
|
|
||||||
select {
|
|
||||||
case resultChannel <- key:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *V4Extractor) SearchKey(ctx context.Context, memory []byte) (string, bool) {
|
|
||||||
for _, keyPattern := range e.keyPatterns {
|
|
||||||
index := len(memory)
|
index := len(memory)
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return "", false
|
return // Exit if context cancelled
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
|
|
||||||
// Find pattern from end to beginning
|
// Find pattern from end to beginning
|
||||||
index = bytes.LastIndex(memory[:index], keyPattern.Pattern)
|
index = bytes.LastIndex(memory[:index], keyPattern)
|
||||||
if index == -1 {
|
if index == -1 {
|
||||||
break // No more matches found
|
break // No more matches found
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we have enough space for the key
|
// Check if we have enough space for the key
|
||||||
keyOffset := index + keyPattern.Offset
|
if index+16+32 > len(memory) {
|
||||||
if keyOffset < 0 || keyOffset+32 > len(memory) {
|
|
||||||
index -= 1
|
index -= 1
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Extract the key data, which is 16 bytes after the pattern and 32 bytes long
|
// Extract the key data, which is 16 bytes after the pattern and 32 bytes long
|
||||||
|
keyOffset := index + 16
|
||||||
keyData := memory[keyOffset : keyOffset+32]
|
keyData := memory[keyOffset : keyOffset+32]
|
||||||
|
|
||||||
// Validate key against database header
|
// Validate key against database header
|
||||||
if e.validator.Validate(keyData) {
|
if e.validator.Validate(keyData) {
|
||||||
log.Debug().
|
select {
|
||||||
Str("pattern", hex.EncodeToString(keyPattern.Pattern)).
|
case resultChannel <- hex.EncodeToString(keyData):
|
||||||
Int("offset", keyPattern.Offset).
|
log.Debug().Msg("Key found: " + hex.EncodeToString(keyData))
|
||||||
Str("key", hex.EncodeToString(keyData)).
|
return
|
||||||
Msg("Key found")
|
default:
|
||||||
return hex.EncodeToString(keyData), true
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
index -= 1
|
index -= 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return "", false
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *V4Extractor) SetValidate(validator *decrypt.Validator) {
|
func (e *V4Extractor) SetValidate(validator *decrypt.Validator) {
|
||||||
e.validator = validator
|
e.validator = validator
|
||||||
}
|
}
|
||||||
|
|
||||||
type KeyPatternInfo struct {
|
|
||||||
Pattern []byte
|
|
||||||
Offset int
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,9 +15,6 @@ type Extractor interface {
|
|||||||
// Extract 从进程中提取密钥
|
// Extract 从进程中提取密钥
|
||||||
Extract(ctx context.Context, proc *model.Process) (string, error)
|
Extract(ctx context.Context, proc *model.Process) (string, error)
|
||||||
|
|
||||||
// SearchKey 在内存中搜索密钥
|
|
||||||
SearchKey(ctx context.Context, memory []byte) (string, bool)
|
|
||||||
|
|
||||||
SetValidate(validator *decrypt.Validator)
|
SetValidate(validator *decrypt.Validator)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package windows
|
package windows
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/sjzar/chatlog/internal/wechat/decrypt"
|
"github.com/sjzar/chatlog/internal/wechat/decrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,11 +12,6 @@ func NewV3Extractor() *V3Extractor {
|
|||||||
return &V3Extractor{}
|
return &V3Extractor{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *V3Extractor) SearchKey(ctx context.Context, memory []byte) (string, bool) {
|
|
||||||
// TODO : Implement the key search logic for V3
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *V3Extractor) SetValidate(validator *decrypt.Validator) {
|
func (e *V3Extractor) SetValidate(validator *decrypt.Validator) {
|
||||||
e.validator = validator
|
e.validator = validator
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
package windows
|
package windows
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/sjzar/chatlog/internal/wechat/decrypt"
|
"github.com/sjzar/chatlog/internal/wechat/decrypt"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,11 +12,6 @@ func NewV4Extractor() *V4Extractor {
|
|||||||
return &V4Extractor{}
|
return &V4Extractor{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *V4Extractor) SearchKey(ctx context.Context, memory []byte) (string, bool) {
|
|
||||||
// TODO : Implement the key search logic for V4
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *V4Extractor) SetValidate(validator *decrypt.Validator) {
|
func (e *V4Extractor) SetValidate(validator *decrypt.Validator) {
|
||||||
e.validator = validator
|
e.validator = validator
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,10 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ProcessNameOfficial = "WeChat"
|
V3ProcessName = "WeChat"
|
||||||
ProcessNameBeta = "Weixin"
|
V4ProcessName = "Weixin"
|
||||||
V3DBFile = "Message/msg_0.db"
|
V3DBFile = "Message/msg_0.db"
|
||||||
V4DBFile = "db_storage/session/session.db"
|
V4DBFile = "db_storage/message/message_0.db"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Detector 实现 macOS 平台的进程检测器
|
// Detector 实现 macOS 平台的进程检测器
|
||||||
@@ -40,7 +40,7 @@ func (d *Detector) FindProcesses() ([]*model.Process, error) {
|
|||||||
var result []*model.Process
|
var result []*model.Process
|
||||||
for _, p := range processes {
|
for _, p := range processes {
|
||||||
name, err := p.Name()
|
name, err := p.Name()
|
||||||
if err != nil || (name != ProcessNameOfficial && name != ProcessNameBeta) {
|
if err != nil || (name != V3ProcessName && name != V4ProcessName) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ func (a *Account) GetKey(ctx context.Context) (string, error) {
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
validator, err := decrypt.NewValidator(process.Platform, process.Version, process.DataDir)
|
validator, err := decrypt.NewValidator(process.DataDir, process.Platform, process.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ const (
|
|||||||
ContactFilePattern = "^contact\\.db$"
|
ContactFilePattern = "^contact\\.db$"
|
||||||
SessionFilePattern = "^session\\.db$"
|
SessionFilePattern = "^session\\.db$"
|
||||||
MediaFilePattern = "^hardlink\\.db$"
|
MediaFilePattern = "^hardlink\\.db$"
|
||||||
VoiceFilePattern = "^media_([0-9]?[0-9])?\\.db$"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// MessageDBInfo 存储消息数据库的信息
|
// MessageDBInfo 存储消息数据库的信息
|
||||||
@@ -31,6 +30,7 @@ type MessageDBInfo struct {
|
|||||||
FilePath string
|
FilePath string
|
||||||
StartTime time.Time
|
StartTime time.Time
|
||||||
EndTime time.Time
|
EndTime time.Time
|
||||||
|
ID2Name map[int]string
|
||||||
}
|
}
|
||||||
|
|
||||||
type DataSource struct {
|
type DataSource struct {
|
||||||
@@ -39,7 +39,6 @@ type DataSource struct {
|
|||||||
contactDb *sql.DB
|
contactDb *sql.DB
|
||||||
sessionDb *sql.DB
|
sessionDb *sql.DB
|
||||||
mediaDb *sql.DB
|
mediaDb *sql.DB
|
||||||
voiceDb []*sql.DB
|
|
||||||
|
|
||||||
// 消息数据库信息
|
// 消息数据库信息
|
||||||
messageFiles []MessageDBInfo
|
messageFiles []MessageDBInfo
|
||||||
@@ -49,7 +48,6 @@ func New(path string) (*DataSource, error) {
|
|||||||
ds := &DataSource{
|
ds := &DataSource{
|
||||||
path: path,
|
path: path,
|
||||||
messageDbs: make(map[string]*sql.DB),
|
messageDbs: make(map[string]*sql.DB),
|
||||||
voiceDb: make([]*sql.DB, 0),
|
|
||||||
messageFiles: make([]MessageDBInfo, 0),
|
messageFiles: make([]MessageDBInfo, 0),
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,9 +63,6 @@ func New(path string) (*DataSource, error) {
|
|||||||
if err := ds.initMediaDb(path); err != nil {
|
if err := ds.initMediaDb(path); err != nil {
|
||||||
return nil, errors.DBInitFailed(err)
|
return nil, errors.DBInitFailed(err)
|
||||||
}
|
}
|
||||||
if err := ds.initVoiceDb(path); err != nil {
|
|
||||||
return nil, errors.DBInitFailed(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ds, nil
|
return ds, nil
|
||||||
}
|
}
|
||||||
@@ -104,10 +99,32 @@ func (ds *DataSource) initMessageDbs(path string) error {
|
|||||||
}
|
}
|
||||||
startTime = time.Unix(timestamp, 0)
|
startTime = time.Unix(timestamp, 0)
|
||||||
|
|
||||||
|
// 获取 ID2Name 映射
|
||||||
|
id2Name := make(map[int]string)
|
||||||
|
rows, err := db.Query("SELECT user_name FROM Name2Id")
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msgf("获取数据库 %s 的 Name2Id 表失败", filePath)
|
||||||
|
db.Close()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
i := 1
|
||||||
|
for rows.Next() {
|
||||||
|
var name string
|
||||||
|
if err := rows.Scan(&name); err != nil {
|
||||||
|
log.Err(err).Msgf("数据库 %s 扫描 Name2Id 行失败", filePath)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
id2Name[i] = name
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
rows.Close()
|
||||||
|
|
||||||
// 保存数据库信息
|
// 保存数据库信息
|
||||||
ds.messageFiles = append(ds.messageFiles, MessageDBInfo{
|
ds.messageFiles = append(ds.messageFiles, MessageDBInfo{
|
||||||
FilePath: filePath,
|
FilePath: filePath,
|
||||||
StartTime: startTime,
|
StartTime: startTime,
|
||||||
|
ID2Name: id2Name,
|
||||||
})
|
})
|
||||||
|
|
||||||
// 保存数据库连接
|
// 保存数据库连接
|
||||||
@@ -179,24 +196,6 @@ func (ds *DataSource) initMediaDb(path string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ds *DataSource) initVoiceDb(path string) error {
|
|
||||||
files, err := util.FindFilesWithPatterns(path, VoiceFilePattern, true)
|
|
||||||
if err != nil {
|
|
||||||
return errors.DBFileNotFound(path, VoiceFilePattern, err)
|
|
||||||
}
|
|
||||||
if len(files) == 0 {
|
|
||||||
return errors.DBFileNotFound(path, VoiceFilePattern, nil)
|
|
||||||
}
|
|
||||||
for _, file := range files {
|
|
||||||
db, err := sql.Open("sqlite3", file)
|
|
||||||
if err != nil {
|
|
||||||
return errors.DBConnectFailed(files[0], err)
|
|
||||||
}
|
|
||||||
ds.voiceDb = append(ds.voiceDb, db)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getDBInfosForTimeRange 获取时间范围内的数据库信息
|
// getDBInfosForTimeRange 获取时间范围内的数据库信息
|
||||||
func (ds *DataSource) getDBInfosForTimeRange(startTime, endTime time.Time) []MessageDBInfo {
|
func (ds *DataSource) getDBInfosForTimeRange(startTime, endTime time.Time) []MessageDBInfo {
|
||||||
var dbs []MessageDBInfo
|
var dbs []MessageDBInfo
|
||||||
@@ -212,7 +211,6 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T
|
|||||||
if talker == "" {
|
if talker == "" {
|
||||||
return nil, errors.ErrTalkerEmpty
|
return nil, errors.ErrTalkerEmpty
|
||||||
}
|
}
|
||||||
log.Debug().Msg(talker)
|
|
||||||
|
|
||||||
// 找到时间范围内的数据库文件
|
// 找到时间范围内的数据库文件
|
||||||
dbInfos := ds.getDBInfosForTimeRange(startTime, endTime)
|
dbInfos := ds.getDBInfosForTimeRange(startTime, endTime)
|
||||||
@@ -240,7 +238,7 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
messages, err := ds.getMessagesFromDB(ctx, db, startTime, endTime, talker)
|
messages, err := ds.getMessagesFromDB(ctx, db, dbInfo, startTime, endTime, talker)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msgf("从数据库 %s 获取消息失败", dbInfo.FilePath)
|
log.Err(err).Msgf("从数据库 %s 获取消息失败", dbInfo.FilePath)
|
||||||
continue
|
continue
|
||||||
@@ -255,7 +253,7 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T
|
|||||||
|
|
||||||
// 对所有消息按时间排序
|
// 对所有消息按时间排序
|
||||||
sort.Slice(totalMessages, func(i, j int) bool {
|
sort.Slice(totalMessages, func(i, j int) bool {
|
||||||
return totalMessages[i].Seq < totalMessages[j].Seq
|
return totalMessages[i].Sequence < totalMessages[j].Sequence
|
||||||
})
|
})
|
||||||
|
|
||||||
// 处理分页
|
// 处理分页
|
||||||
@@ -285,30 +283,15 @@ func (ds *DataSource) getMessagesSingleFile(ctx context.Context, dbInfo MessageD
|
|||||||
talkerMd5 := hex.EncodeToString(_talkerMd5Bytes[:])
|
talkerMd5 := hex.EncodeToString(_talkerMd5Bytes[:])
|
||||||
tableName := "Msg_" + talkerMd5
|
tableName := "Msg_" + talkerMd5
|
||||||
|
|
||||||
// 检查表是否存在
|
|
||||||
var exists bool
|
|
||||||
err := db.QueryRowContext(ctx,
|
|
||||||
"SELECT 1 FROM sqlite_master WHERE type='table' AND name=?",
|
|
||||||
tableName).Scan(&exists)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
if err == sql.ErrNoRows {
|
|
||||||
// 表不存在,返回空结果
|
|
||||||
return []*model.Message{}, nil
|
|
||||||
}
|
|
||||||
return nil, errors.QueryFailed("", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 构建查询条件
|
// 构建查询条件
|
||||||
conditions := []string{"create_time >= ? AND create_time <= ?"}
|
conditions := []string{"create_time >= ? AND create_time <= ?"}
|
||||||
args := []interface{}{startTime.Unix(), endTime.Unix()}
|
args := []interface{}{startTime.Unix(), endTime.Unix()}
|
||||||
|
|
||||||
query := fmt.Sprintf(`
|
query := fmt.Sprintf(`
|
||||||
SELECT m.sort_seq, m.server_id, m.local_type, n.user_name, m.create_time, m.message_content, m.packed_info_data, m.status
|
SELECT sort_seq, local_type, real_sender_id, create_time, message_content, packed_info_data, status
|
||||||
FROM %s m
|
FROM %s
|
||||||
LEFT JOIN Name2Id n ON m.real_sender_id = n.rowid
|
|
||||||
WHERE %s
|
WHERE %s
|
||||||
ORDER BY m.sort_seq ASC
|
ORDER BY sort_seq ASC
|
||||||
`, tableName, strings.Join(conditions, " AND "))
|
`, tableName, strings.Join(conditions, " AND "))
|
||||||
|
|
||||||
if limit > 0 {
|
if limit > 0 {
|
||||||
@@ -327,14 +310,14 @@ func (ds *DataSource) getMessagesSingleFile(ctx context.Context, dbInfo MessageD
|
|||||||
|
|
||||||
// 处理查询结果
|
// 处理查询结果
|
||||||
messages := []*model.Message{}
|
messages := []*model.Message{}
|
||||||
|
isChatRoom := strings.HasSuffix(talker, "@chatroom")
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var msg model.MessageV4
|
var msg model.MessageV4
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&msg.SortSeq,
|
&msg.SortSeq,
|
||||||
&msg.ServerID,
|
|
||||||
&msg.LocalType,
|
&msg.LocalType,
|
||||||
&msg.UserName,
|
&msg.RealSenderID,
|
||||||
&msg.CreateTime,
|
&msg.CreateTime,
|
||||||
&msg.MessageContent,
|
&msg.MessageContent,
|
||||||
&msg.PackedInfoData,
|
&msg.PackedInfoData,
|
||||||
@@ -344,14 +327,14 @@ func (ds *DataSource) getMessagesSingleFile(ctx context.Context, dbInfo MessageD
|
|||||||
return nil, errors.ScanRowFailed(err)
|
return nil, errors.ScanRowFailed(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
messages = append(messages, msg.Wrap(talker))
|
messages = append(messages, msg.Wrap(dbInfo.ID2Name, isChatRoom))
|
||||||
}
|
}
|
||||||
|
|
||||||
return messages, nil
|
return messages, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// getMessagesFromDB 从数据库获取消息
|
// getMessagesFromDB 从数据库获取消息
|
||||||
func (ds *DataSource) getMessagesFromDB(ctx context.Context, db *sql.DB, startTime, endTime time.Time, talker string) ([]*model.Message, error) {
|
func (ds *DataSource) getMessagesFromDB(ctx context.Context, db *sql.DB, dbInfo MessageDBInfo, startTime, endTime time.Time, talker string) ([]*model.Message, error) {
|
||||||
// 构建表名
|
// 构建表名
|
||||||
_talkerMd5Bytes := md5.Sum([]byte(talker))
|
_talkerMd5Bytes := md5.Sum([]byte(talker))
|
||||||
talkerMd5 := hex.EncodeToString(_talkerMd5Bytes[:])
|
talkerMd5 := hex.EncodeToString(_talkerMd5Bytes[:])
|
||||||
@@ -376,11 +359,10 @@ func (ds *DataSource) getMessagesFromDB(ctx context.Context, db *sql.DB, startTi
|
|||||||
args := []interface{}{startTime.Unix(), endTime.Unix()}
|
args := []interface{}{startTime.Unix(), endTime.Unix()}
|
||||||
|
|
||||||
query := fmt.Sprintf(`
|
query := fmt.Sprintf(`
|
||||||
SELECT m.sort_seq, m.server_id, m.local_type, n.user_name, m.create_time, m.message_content, m.packed_info_data, m.status
|
SELECT sort_seq, local_type, real_sender_id, create_time, message_content, packed_info_data, status
|
||||||
FROM %s m
|
FROM %s
|
||||||
LEFT JOIN Name2Id n ON m.real_sender_id = n.rowid
|
|
||||||
WHERE %s
|
WHERE %s
|
||||||
ORDER BY m.sort_seq ASC
|
ORDER BY sort_seq ASC
|
||||||
`, tableName, strings.Join(conditions, " AND "))
|
`, tableName, strings.Join(conditions, " AND "))
|
||||||
|
|
||||||
// 执行查询
|
// 执行查询
|
||||||
@@ -396,14 +378,14 @@ func (ds *DataSource) getMessagesFromDB(ctx context.Context, db *sql.DB, startTi
|
|||||||
|
|
||||||
// 处理查询结果
|
// 处理查询结果
|
||||||
messages := []*model.Message{}
|
messages := []*model.Message{}
|
||||||
|
isChatRoom := strings.HasSuffix(talker, "@chatroom")
|
||||||
|
|
||||||
for rows.Next() {
|
for rows.Next() {
|
||||||
var msg model.MessageV4
|
var msg model.MessageV4
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&msg.SortSeq,
|
&msg.SortSeq,
|
||||||
&msg.ServerID,
|
|
||||||
&msg.LocalType,
|
&msg.LocalType,
|
||||||
&msg.UserName,
|
&msg.RealSenderID,
|
||||||
&msg.CreateTime,
|
&msg.CreateTime,
|
||||||
&msg.MessageContent,
|
&msg.MessageContent,
|
||||||
&msg.PackedInfoData,
|
&msg.PackedInfoData,
|
||||||
@@ -413,7 +395,7 @@ func (ds *DataSource) getMessagesFromDB(ctx context.Context, db *sql.DB, startTi
|
|||||||
return nil, errors.ScanRowFailed(err)
|
return nil, errors.ScanRowFailed(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
messages = append(messages, msg.Wrap(talker))
|
messages = append(messages, msg.Wrap(dbInfo.ID2Name, isChatRoom))
|
||||||
}
|
}
|
||||||
|
|
||||||
return messages, nil
|
return messages, nil
|
||||||
@@ -646,6 +628,10 @@ func (ds *DataSource) GetMedia(ctx context.Context, _type string, key string) (*
|
|||||||
return nil, errors.ErrKeyEmpty
|
return nil, errors.ErrKeyEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if len(key) != 32 {
|
||||||
|
return nil, errors.ErrKeyLengthMust32
|
||||||
|
}
|
||||||
|
|
||||||
var table string
|
var table string
|
||||||
switch _type {
|
switch _type {
|
||||||
case "image":
|
case "image":
|
||||||
@@ -654,8 +640,6 @@ func (ds *DataSource) GetMedia(ctx context.Context, _type string, key string) (*
|
|||||||
table = "video_hardlink_info_v3"
|
table = "video_hardlink_info_v3"
|
||||||
case "file":
|
case "file":
|
||||||
table = "file_hardlink_info_v3"
|
table = "file_hardlink_info_v3"
|
||||||
case "voice":
|
|
||||||
return ds.GetVoice(ctx, key)
|
|
||||||
default:
|
default:
|
||||||
return nil, errors.MediaTypeUnsupported(_type)
|
return nil, errors.MediaTypeUnsupported(_type)
|
||||||
}
|
}
|
||||||
@@ -714,46 +698,6 @@ func (ds *DataSource) GetMedia(ctx context.Context, _type string, key string) (*
|
|||||||
return media, nil
|
return media, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ds *DataSource) GetVoice(ctx context.Context, key string) (*model.Media, error) {
|
|
||||||
if key == "" {
|
|
||||||
return nil, errors.ErrKeyEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
query := `
|
|
||||||
SELECT voice_data
|
|
||||||
FROM VoiceInfo
|
|
||||||
WHERE svr_id = ?
|
|
||||||
`
|
|
||||||
args := []interface{}{key}
|
|
||||||
|
|
||||||
for _, db := range ds.voiceDb {
|
|
||||||
rows, err := db.QueryContext(ctx, query, args...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.QueryFailed(query, err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
var voiceData []byte
|
|
||||||
err := rows.Scan(
|
|
||||||
&voiceData,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.ScanRowFailed(err)
|
|
||||||
}
|
|
||||||
if len(voiceData) > 0 {
|
|
||||||
return &model.Media{
|
|
||||||
Type: "voice",
|
|
||||||
Key: key,
|
|
||||||
Data: voiceData,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, errors.ErrMediaNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
func (ds *DataSource) Close() error {
|
func (ds *DataSource) Close() error {
|
||||||
var errs []error
|
var errs []error
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ const (
|
|||||||
ImageFilePattern = "^HardLinkImage\\.db$"
|
ImageFilePattern = "^HardLinkImage\\.db$"
|
||||||
VideoFilePattern = "^HardLinkVideo\\.db$"
|
VideoFilePattern = "^HardLinkVideo\\.db$"
|
||||||
FileFilePattern = "^HardLinkFile\\.db$"
|
FileFilePattern = "^HardLinkFile\\.db$"
|
||||||
VoiceFilePattern = "^MediaMSG([0-9])?\\.db$"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// MessageDBInfo 保存消息数据库的信息
|
// MessageDBInfo 保存消息数据库的信息
|
||||||
@@ -47,7 +46,6 @@ type DataSource struct {
|
|||||||
imageDb *sql.DB
|
imageDb *sql.DB
|
||||||
videoDb *sql.DB
|
videoDb *sql.DB
|
||||||
fileDb *sql.DB
|
fileDb *sql.DB
|
||||||
voiceDb []*sql.DB
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// New 创建一个新的 WindowsV3DataSource
|
// New 创建一个新的 WindowsV3DataSource
|
||||||
@@ -55,7 +53,6 @@ func New(path string) (*DataSource, error) {
|
|||||||
ds := &DataSource{
|
ds := &DataSource{
|
||||||
messageFiles: make([]MessageDBInfo, 0),
|
messageFiles: make([]MessageDBInfo, 0),
|
||||||
messageDbs: make(map[string]*sql.DB),
|
messageDbs: make(map[string]*sql.DB),
|
||||||
voiceDb: make([]*sql.DB, 0),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 初始化消息数据库
|
// 初始化消息数据库
|
||||||
@@ -72,10 +69,6 @@ func New(path string) (*DataSource, error) {
|
|||||||
return nil, errors.DBInitFailed(err)
|
return nil, errors.DBInitFailed(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := ds.initVoiceDb(path); err != nil {
|
|
||||||
return nil, errors.DBInitFailed(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ds, nil
|
return ds, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,24 +238,6 @@ func (ds *DataSource) initMediaDb(path string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ds *DataSource) initVoiceDb(path string) error {
|
|
||||||
files, err := util.FindFilesWithPatterns(path, VoiceFilePattern, true)
|
|
||||||
if err != nil {
|
|
||||||
return errors.DBFileNotFound(path, VoiceFilePattern, err)
|
|
||||||
}
|
|
||||||
if len(files) == 0 {
|
|
||||||
return errors.DBFileNotFound(path, VoiceFilePattern, nil)
|
|
||||||
}
|
|
||||||
for _, file := range files {
|
|
||||||
db, err := sql.Open("sqlite3", file)
|
|
||||||
if err != nil {
|
|
||||||
return errors.DBConnectFailed(files[0], err)
|
|
||||||
}
|
|
||||||
ds.voiceDb = append(ds.voiceDb, db)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// getDBInfosForTimeRange 获取时间范围内的数据库信息
|
// getDBInfosForTimeRange 获取时间范围内的数据库信息
|
||||||
func (ds *DataSource) getDBInfosForTimeRange(startTime, endTime time.Time) []MessageDBInfo {
|
func (ds *DataSource) getDBInfosForTimeRange(startTime, endTime time.Time) []MessageDBInfo {
|
||||||
var dbs []MessageDBInfo
|
var dbs []MessageDBInfo
|
||||||
@@ -318,7 +293,7 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T
|
|||||||
}
|
}
|
||||||
|
|
||||||
query := fmt.Sprintf(`
|
query := fmt.Sprintf(`
|
||||||
SELECT MsgSvrID, Sequence, CreateTime, StrTalker, IsSender,
|
SELECT Sequence, CreateTime, TalkerId, StrTalker, IsSender,
|
||||||
Type, SubType, StrContent, CompressContent, BytesExtra
|
Type, SubType, StrContent, CompressContent, BytesExtra
|
||||||
FROM MSG
|
FROM MSG
|
||||||
WHERE %s
|
WHERE %s
|
||||||
@@ -339,9 +314,9 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T
|
|||||||
var bytesExtra []byte
|
var bytesExtra []byte
|
||||||
|
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&msg.MsgSvrID,
|
|
||||||
&msg.Sequence,
|
&msg.Sequence,
|
||||||
&msg.CreateTime,
|
&msg.CreateTime,
|
||||||
|
&msg.TalkerID,
|
||||||
&msg.StrTalker,
|
&msg.StrTalker,
|
||||||
&msg.IsSender,
|
&msg.IsSender,
|
||||||
&msg.Type,
|
&msg.Type,
|
||||||
@@ -368,7 +343,7 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T
|
|||||||
|
|
||||||
// 对所有消息按时间排序
|
// 对所有消息按时间排序
|
||||||
sort.Slice(totalMessages, func(i, j int) bool {
|
sort.Slice(totalMessages, func(i, j int) bool {
|
||||||
return totalMessages[i].Seq < totalMessages[j].Seq
|
return totalMessages[i].Sequence < totalMessages[j].Sequence
|
||||||
})
|
})
|
||||||
|
|
||||||
// 处理分页
|
// 处理分页
|
||||||
@@ -403,7 +378,7 @@ func (ds *DataSource) getMessagesSingleFile(ctx context.Context, dbInfo MessageD
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
query := fmt.Sprintf(`
|
query := fmt.Sprintf(`
|
||||||
SELECT MsgSvrID, Sequence, CreateTime, StrTalker, IsSender,
|
SELECT Sequence, CreateTime, TalkerId, StrTalker, IsSender,
|
||||||
Type, SubType, StrContent, CompressContent, BytesExtra
|
Type, SubType, StrContent, CompressContent, BytesExtra
|
||||||
FROM MSG
|
FROM MSG
|
||||||
WHERE %s
|
WHERE %s
|
||||||
@@ -432,9 +407,9 @@ func (ds *DataSource) getMessagesSingleFile(ctx context.Context, dbInfo MessageD
|
|||||||
var compressContent []byte
|
var compressContent []byte
|
||||||
var bytesExtra []byte
|
var bytesExtra []byte
|
||||||
err := rows.Scan(
|
err := rows.Scan(
|
||||||
&msg.MsgSvrID,
|
|
||||||
&msg.Sequence,
|
&msg.Sequence,
|
||||||
&msg.CreateTime,
|
&msg.CreateTime,
|
||||||
|
&msg.TalkerID,
|
||||||
&msg.StrTalker,
|
&msg.StrTalker,
|
||||||
&msg.IsSender,
|
&msg.IsSender,
|
||||||
&msg.Type,
|
&msg.Type,
|
||||||
@@ -679,10 +654,6 @@ func (ds *DataSource) GetMedia(ctx context.Context, _type string, key string) (*
|
|||||||
return nil, errors.ErrKeyEmpty
|
return nil, errors.ErrKeyEmpty
|
||||||
}
|
}
|
||||||
|
|
||||||
if _type == "voice" {
|
|
||||||
return ds.GetVoice(ctx, key)
|
|
||||||
}
|
|
||||||
|
|
||||||
md5key, err := hex.DecodeString(key)
|
md5key, err := hex.DecodeString(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, errors.DecodeKeyFailed(err)
|
return nil, errors.DecodeKeyFailed(err)
|
||||||
@@ -756,46 +727,6 @@ func (ds *DataSource) GetMedia(ctx context.Context, _type string, key string) (*
|
|||||||
return media, nil
|
return media, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ds *DataSource) GetVoice(ctx context.Context, key string) (*model.Media, error) {
|
|
||||||
if key == "" {
|
|
||||||
return nil, errors.ErrKeyEmpty
|
|
||||||
}
|
|
||||||
|
|
||||||
query := `
|
|
||||||
SELECT Buf
|
|
||||||
FROM Media
|
|
||||||
WHERE Reserved0 = ?
|
|
||||||
`
|
|
||||||
args := []interface{}{key}
|
|
||||||
|
|
||||||
for _, db := range ds.voiceDb {
|
|
||||||
rows, err := db.QueryContext(ctx, query, args...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.QueryFailed(query, err)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
for rows.Next() {
|
|
||||||
var voiceData []byte
|
|
||||||
err := rows.Scan(
|
|
||||||
&voiceData,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, errors.ScanRowFailed(err)
|
|
||||||
}
|
|
||||||
if len(voiceData) > 0 {
|
|
||||||
return &model.Media{
|
|
||||||
Type: "voice",
|
|
||||||
Key: key,
|
|
||||||
Data: voiceData,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, errors.ErrMediaNotFound
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close 实现 DataSource 接口的 Close 方法
|
// Close 实现 DataSource 接口的 Close 方法
|
||||||
func (ds *DataSource) Close() error {
|
func (ds *DataSource) Close() error {
|
||||||
var errs []error
|
var errs []error
|
||||||
|
|||||||
@@ -41,24 +41,28 @@ func (r *Repository) EnrichMessages(ctx context.Context, messages []*model.Messa
|
|||||||
|
|
||||||
// enrichMessage 补充单条消息的额外信息
|
// enrichMessage 补充单条消息的额外信息
|
||||||
func (r *Repository) enrichMessage(msg *model.Message) {
|
func (r *Repository) enrichMessage(msg *model.Message) {
|
||||||
|
talker := msg.Talker
|
||||||
|
|
||||||
// 处理群聊消息
|
// 处理群聊消息
|
||||||
if msg.IsChatRoom {
|
if msg.IsChatRoom {
|
||||||
|
talker = msg.ChatRoomSender
|
||||||
|
|
||||||
// 补充群聊名称
|
// 补充群聊名称
|
||||||
if chatRoom, ok := r.chatRoomCache[msg.Talker]; ok {
|
if chatRoom, ok := r.chatRoomCache[msg.Talker]; ok {
|
||||||
msg.TalkerName = chatRoom.DisplayName()
|
msg.ChatRoomName = chatRoom.DisplayName()
|
||||||
|
|
||||||
// 补充发送者在群里的显示名称
|
// 补充发送者在群里的显示名称
|
||||||
if displayName, ok := chatRoom.User2DisplayName[msg.Sender]; ok {
|
if displayName, ok := chatRoom.User2DisplayName[talker]; ok {
|
||||||
msg.SenderName = displayName
|
msg.DisplayName = displayName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 如果不是自己发送的消息且还没有显示名称,尝试补充发送者信息
|
// 如果不是自己发送的消息且还没有显示名称,尝试补充发送者信息
|
||||||
if msg.SenderName == "" && !msg.IsSelf {
|
if msg.DisplayName == "" && msg.IsSender != 1 {
|
||||||
contact := r.getFullContact(msg.Sender)
|
contact := r.getFullContact(talker)
|
||||||
if contact != nil {
|
if contact != nil {
|
||||||
msg.SenderName = contact.DisplayName()
|
msg.DisplayName = contact.DisplayName()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
package silk
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/sjzar/go-lame"
|
|
||||||
"github.com/sjzar/go-silk"
|
|
||||||
)
|
|
||||||
|
|
||||||
func Silk2MP3(data []byte) ([]byte, error) {
|
|
||||||
|
|
||||||
sd := silk.SilkInit()
|
|
||||||
defer sd.Close()
|
|
||||||
|
|
||||||
pcmdata := sd.Decode(data)
|
|
||||||
if len(pcmdata) == 0 {
|
|
||||||
return nil, fmt.Errorf("silk decode failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
le := lame.Init()
|
|
||||||
defer le.Close()
|
|
||||||
|
|
||||||
le.SetInSamplerate(24000)
|
|
||||||
le.SetOutSamplerate(24000)
|
|
||||||
le.SetNumChannels(1)
|
|
||||||
le.SetBitrate(16)
|
|
||||||
// IMPORTANT!
|
|
||||||
le.InitParams()
|
|
||||||
|
|
||||||
mp3data := le.Encode(pcmdata)
|
|
||||||
if len(mp3data) == 0 {
|
|
||||||
return nil, fmt.Errorf("mp3 encode failed")
|
|
||||||
}
|
|
||||||
|
|
||||||
return mp3data, nil
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user