From f2aa923e9913b2d91f0f557a65b80756482fba9e Mon Sep 17 00:00:00 2001 From: Sarv Date: Sat, 12 Apr 2025 03:10:32 +0800 Subject: [PATCH] support voice message (#31) --- go.mod | 30 +++--- go.sum | 61 ++++++------ internal/chatlog/http/route.go | 22 ++++- internal/errors/errors.go | 2 +- internal/model/contact.go | 27 ------ internal/model/contact_darwinv3.go | 27 ------ internal/model/contact_v4.go | 19 ---- internal/model/media.go | 1 + internal/model/mediamessage.go | 54 ++++++++++- internal/model/message.go | 12 +-- internal/model/message_v3.go | 7 ++ internal/model/message_v4.go | 7 ++ internal/wechatdb/datasource/v4/datasource.go | 95 +++++++++++++++++-- .../datasource/windowsv3/datasource.go | 75 ++++++++++++++- pkg/util/silk/silk.go | 36 +++++++ 15 files changed, 338 insertions(+), 137 deletions(-) create mode 100644 pkg/util/silk/silk.go diff --git a/go.mod b/go.mod index b894c0f..e196dc9 100644 --- a/go.mod +++ b/go.mod @@ -7,16 +7,18 @@ require ( github.com/gin-gonic/gin v1.10.0 github.com/google/uuid v1.6.0 github.com/klauspost/compress v1.18.0 - github.com/mattn/go-sqlite3 v1.14.24 + github.com/mattn/go-sqlite3 v1.14.27 github.com/pierrec/lz4/v4 v4.1.22 - github.com/rivo/tview v0.0.0-20250325173046-7b72abf45814 + github.com/rivo/tview v0.0.0-20250330220935-949945f8d922 github.com/rs/zerolog v1.34.0 - github.com/shirou/gopsutil/v4 v4.25.2 + github.com/shirou/gopsutil/v4 v4.25.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/viper v1.20.1 - golang.org/x/crypto v0.36.0 - golang.org/x/sys v0.31.0 + golang.org/x/crypto v0.37.0 + golang.org/x/sys v0.32.0 google.golang.org/protobuf v1.36.6 howett.net/plist v1.0.1 ) @@ -26,14 +28,14 @@ require ( github.com/bytedance/sonic/loader v0.2.4 // indirect github.com/cloudwego/base64x v0.1.5 // indirect github.com/ebitengine/purego v0.8.2 // indirect - github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.8 // indirect github.com/gdamore/encoding v1.0.1 // indirect - github.com/gin-contrib/sse v1.0.0 // indirect + github.com/gin-contrib/sse v1.1.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/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.25.0 // indirect + github.com/go-playground/validator/v10 v10.26.0 // indirect github.com/go-viper/mapstructure/v2 v2.2.1 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect @@ -42,12 +44,12 @@ require ( github.com/leodido/go-urn v1.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect - github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-runewidth v0.0.16 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/rivo/uniseg v0.4.7 // indirect github.com/sagikazarmark/locafero v0.9.0 // indirect @@ -62,9 +64,9 @@ require ( github.com/ugorji/go/codec v1.2.12 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect go.uber.org/multierr v1.11.0 // indirect - golang.org/x/arch v0.15.0 // indirect - golang.org/x/net v0.38.0 // indirect - golang.org/x/term v0.30.0 // indirect - golang.org/x/text v0.23.0 // indirect + golang.org/x/arch v0.16.0 // indirect + golang.org/x/net v0.39.0 // indirect + golang.org/x/term v0.31.0 // indirect + golang.org/x/text v0.24.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 98ebcbb..f6135b0 100644 --- a/go.sum +++ b/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/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= -github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= +github.com/fsnotify/fsnotify v1.9.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/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= 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/tcell/v2 v2.8.1 h1:KPNxyqclpWpWQlPLx6Xui1pMk8S+7+R37h3g07997NU= github.com/gdamore/tcell/v2 v2.8.1/go.mod h1:bj8ori1BG3OYMjmb3IklZVWfZUJ1UBQt9JXrOCOhGWw= -github.com/gin-contrib/sse v1.0.0 h1:y3bT1mUWUxDpW4JLQg/HnTqV4rozuW4tC9eFKTxYI9E= -github.com/gin-contrib/sse v1.0.0/go.mod h1:zNuFdwarAygJBht0NTKiSi3jRf6RbqeILZ9Sp6Slhe0= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= 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/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/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/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8= -github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= +github.com/go-playground/validator/v10 v10.26.0 h1:SP05Nqhjcvz81uJaRfEV0YBSSSGMc/iMaVtFbr3Sw2k= +github.com/go-playground/validator/v10 v10.26.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= 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/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= @@ -70,23 +70,24 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc= github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-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.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM= -github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/mattn/go-sqlite3 v1.14.27 h1:drZCnuvf37yPfs95E5jd9s3XhdVWLal+6BOK6qrv6IU= +github.com/mattn/go-sqlite3 v1.14.27/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-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= 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/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= -github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= github.com/pierrec/lz4/v4 v4.1.22 h1:cKFw6uJDK+/gfw5BcDL0JL5aBsAFdsIT18eRtLj7VIU= github.com/pierrec/lz4/v4 v4.1.22/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -94,8 +95,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/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= -github.com/rivo/tview v0.0.0-20250325173046-7b72abf45814 h1:pJIO3sp+rkDbJTeqqpe2Oihq3hegiM5ASvsd6S0pvjg= -github.com/rivo/tview v0.0.0-20250325173046-7b72abf45814/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss= +github.com/rivo/tview v0.0.0-20250330220935-949945f8d922 h1:SMyqkaRfpE8ZQUSRTZKO3uN84xov++OGa+e3NCksaQw= +github.com/rivo/tview v0.0.0-20250330220935-949945f8d922/go.mod h1:02iFIz7K/A9jGCvrizLPvoqr4cEIx7q54RH5Qudkrss= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.4.3/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= @@ -108,10 +109,14 @@ 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/sagikazarmark/locafero v0.9.0 h1:GbgQGNtTrEmddYDSAH9QLRyfAHY12md+8YFTqyMTC9k= github.com/sagikazarmark/locafero v0.9.0/go.mod h1:UBUyz37V+EdMS3hDF3QWIiVr/2dPrx49OMO0Bn0hJqk= -github.com/shirou/gopsutil/v4 v4.25.2 h1:NMscG3l2CqtWFS86kj3vP7soOczqrQYIEhO/pMvvQkk= -github.com/shirou/gopsutil/v4 v4.25.2/go.mod h1:34gBYJzyqCDT11b6bMHP0XCvWeU3J61XRT7a2EmCRTA= +github.com/shirou/gopsutil/v4 v4.25.3 h1:SeA68lsu8gLggyMbmCn8cmp97V1TI9ld9sVzAUcKcKE= +github.com/shirou/gopsutil/v4 v4.25.3/go.mod h1:xbuxyoZj+UsgnZrENu3lQivsngRR5BdjbJwf2fv4szA= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= 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/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= github.com/spf13/afero v1.14.0 h1:9tH6MapGnn/j0eb0yIXiLjERO8RB6xIVZRDCX7PtqWA= @@ -127,13 +132,11 @@ 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.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.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.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.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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 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/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= @@ -151,15 +154,15 @@ github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo 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/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/arch v0.15.0 h1:QtOrQd0bTUnhNVNndMpLHNWrDmYzZ2KDqSrEymqInZw= -golang.org/x/arch v0.15.0/go.mod h1:JmwW7aLIoRUKgaTzhkiEFxvcEiQGyOg9BMonBJUS7EE= +golang.org/x/arch v0.16.0 h1:foMtLTdyOmIniqWCHjY6+JxuC54XP1fDwx4N0ASyW+U= +golang.org/x/arch v0.16.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-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.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.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= -golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE= +golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc= 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.12.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= @@ -173,8 +176,8 @@ golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= -golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8= -golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= +golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY= +golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -199,8 +202,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.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.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= -golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= +golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20= +golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 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-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= @@ -210,8 +213,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.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.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= -golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/term v0.31.0 h1:erwDkOK1Msy6offm1mOgvspSkslFnIGsFnxOKoufg3o= +golang.org/x/term v0.31.0/go.mod h1:R4BeIy7D95HzImkxGkTW1UQTtP54tio2RyHz7PwK0aw= 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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= @@ -221,8 +224,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.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.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= -golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= 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.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= diff --git a/internal/chatlog/http/route.go b/internal/chatlog/http/route.go index 8e06703..e78cfe6 100644 --- a/internal/chatlog/http/route.go +++ b/internal/chatlog/http/route.go @@ -12,6 +12,7 @@ import ( "github.com/sjzar/chatlog/internal/errors" "github.com/sjzar/chatlog/pkg/util" "github.com/sjzar/chatlog/pkg/util/dat2img" + "github.com/sjzar/chatlog/pkg/util/silk" "github.com/gin-gonic/gin" ) @@ -34,6 +35,7 @@ func (s *Service) initRouter() { router.GET("/image/:key", s.GetImage) router.GET("/video/:key", s.GetVideo) router.GET("/file/:key", s.GetFile) + router.GET("/voice/:key", s.GetVoice) router.GET("/data/*path", s.GetMediaData) // MCP Server @@ -272,6 +274,9 @@ func (s *Service) GetVideo(c *gin.Context) { func (s *Service) GetFile(c *gin.Context) { s.GetMedia(c, "file") } +func (s *Service) GetVoice(c *gin.Context) { + s.GetMedia(c, "voice") +} func (s *Service) GetMedia(c *gin.Context, _type string) { key := c.Param("key") @@ -291,7 +296,13 @@ func (s *Service) GetMedia(c *gin.Context, _type string) { return } - c.Redirect(http.StatusFound, "/data/"+media.Path) + switch media.Type { + case "voice": + s.HandleVoice(c, media.Data) + default: + c.Redirect(http.StatusFound, "/data/"+media.Path) + } + } func (s *Service) GetMediaData(c *gin.Context) { @@ -343,3 +354,12 @@ func (s *Service) HandleDatFile(c *gin.Context, path string) { c.File(path) } } + +func (s *Service) HandleVoice(c *gin.Context, data []byte) { + out, err := silk.Silk2MP3(data) + if err != nil { + c.Data(http.StatusOK, "audio/silk", data) + return + } + c.Data(http.StatusOK, "audio/mp3", out) +} diff --git a/internal/errors/errors.go b/internal/errors/errors.go index d6d80bd..68d2b3d 100644 --- a/internal/errors/errors.go +++ b/internal/errors/errors.go @@ -112,7 +112,7 @@ func RootCause(err error) error { func Err(c *gin.Context, err error) { if appErr, ok := err.(*Error); ok { - c.JSON(appErr.Code, appErr) + c.JSON(appErr.Code, appErr.Error()) return } diff --git a/internal/model/contact.go b/internal/model/contact.go index 6fe9b1c..67b7359 100644 --- a/internal/model/contact.go +++ b/internal/model/contact.go @@ -47,33 +47,6 @@ type ContactV3 struct { Remark string `json:"Remark"` NickName string `json:"NickName"` Reserved1 int `json:"Reserved1"` // 1 自己好友或自己加入的群聊; 0 群聊成员(非好友) - - // EncryptUserName string `json:"EncryptUserName"` - // DelFlag int `json:"DelFlag"` - // Type int `json:"Type"` - // VerifyFlag int `json:"VerifyFlag"` - // Reserved2 int `json:"Reserved2"` - // Reserved3 string `json:"Reserved3"` - // Reserved4 string `json:"Reserved4"` - // LabelIDList string `json:"LabelIDList"` - // DomainList string `json:"DomainList"` - // ChatRoomType int `json:"ChatRoomType"` - // PYInitial string `json:"PYInitial"` - // QuanPin string `json:"QuanPin"` - // RemarkPYInitial string `json:"RemarkPYInitial"` - // RemarkQuanPin string `json:"RemarkQuanPin"` - // BigHeadImgUrl string `json:"BigHeadImgUrl"` - // SmallHeadImgUrl string `json:"SmallHeadImgUrl"` - // HeadImgMd5 string `json:"HeadImgMd5"` - // ChatRoomNotify int `json:"ChatRoomNotify"` - // Reserved5 int `json:"Reserved5"` - // Reserved6 string `json:"Reserved6"` - // Reserved7 string `json:"Reserved7"` - // ExtraBuf []byte `json:"ExtraBuf"` - // Reserved8 int `json:"Reserved8"` - // Reserved9 int `json:"Reserved9"` - // Reserved10 string `json:"Reserved10"` - // Reserved11 string `json:"Reserved11"` } func (c *ContactV3) Wrap() *Contact { diff --git a/internal/model/contact_darwinv3.go b/internal/model/contact_darwinv3.go index df5157f..ee51369 100644 --- a/internal/model/contact_darwinv3.go +++ b/internal/model/contact_darwinv3.go @@ -40,33 +40,6 @@ type ContactDarwinV3 struct { M_nsRemark string `json:"m_nsRemark"` M_uiSex int `json:"m_uiSex"` M_nsAliasName string `json:"m_nsAliasName"` - - // M_uiConType int `json:"m_uiConType"` - // M_nsShortPY string `json:"m_nsShortPY"` - // M_nsRemarkPYFull string `json:"m_nsRemarkPYFull"` - // M_nsRemarkPYShort string `json:"m_nsRemarkPYShort"` - // M_uiCertificationFlag int `json:"m_uiCertificationFlag"` - // M_uiType int `json:"m_uiType"` // 本来想拿这个字段来区分是否是好友,但是数据比较乱,好在 darwin v3 Contact 表中没有群聊成员 - // M_nsImgStatus string `json:"m_nsImgStatus"` - // M_uiImgKey int `json:"m_uiImgKey"` - // M_nsHeadImgUrl string `json:"m_nsHeadImgUrl"` - // M_nsHeadHDImgUrl string `json:"m_nsHeadHDImgUrl"` - // M_nsHeadHDMd5 string `json:"m_nsHeadHDMd5"` - // M_nsChatRoomMemList string `json:"m_nsChatRoomMemList"` - // M_nsChatRoomAdminList string `json:"m_nsChatRoomAdminList"` - // M_uiChatRoomStatus int `json:"m_uiChatRoomStatus"` - // M_nsChatRoomDesc string `json:"m_nsChatRoomDesc"` - // M_nsDraft string `json:"m_nsDraft"` - // M_nsBrandIconUrl string `json:"m_nsBrandIconUrl"` - // M_nsGoogleContactName string `json:"m_nsGoogleContactName"` - // M_nsEncodeUserName string `json:"m_nsEncodeUserName"` - // M_uiChatRoomVersion int `json:"m_uiChatRoomVersion"` - // M_uiChatRoomMaxCount int `json:"m_uiChatRoomMaxCount"` - // M_uiChatRoomType int `json:"m_uiChatRoomType"` - // M_patSuffix string `json:"m_patSuffix"` - // RichChatRoomDesc string `json:"richChatRoomDesc"` - // Packed_WCContactData string `json:"_packed_WCContactData"` - // OpenIMInfo string `json:"openIMInfo"` } func (c *ContactDarwinV3) Wrap() *Contact { diff --git a/internal/model/contact_v4.go b/internal/model/contact_v4.go index e713843..0d03882 100644 --- a/internal/model/contact_v4.go +++ b/internal/model/contact_v4.go @@ -30,25 +30,6 @@ type ContactV4 struct { Remark string `json:"remark"` NickName string `json:"nick_name"` LocalType int `json:"local_type"` // 2 群聊; 3 群聊成员(非好友); 5,6 企业微信; - - // ID int `json:"id"` - - // EncryptUserName string `json:"encrypt_username"` - // Flag int `json:"flag"` - // DeleteFlag int `json:"delete_flag"` - // VerifyFlag int `json:"verify_flag"` - // RemarkQuanPin string `json:"remark_quan_pin"` - // RemarkPinYinInitial string `json:"remark_pin_yin_initial"` - // PinYinInitial string `json:"pin_yin_initial"` - // QuanPin string `json:"quan_pin"` - // BigHeadUrl string `json:"big_head_url"` - // SmallHeadUrl string `json:"small_head_url"` - // HeadImgMd5 string `json:"head_img_md5"` - // ChatRoomNotify int `json:"chat_room_notify"` - // IsInChatRoom int `json:"is_in_chat_room"` - // Description string `json:"description"` - // ExtraBuffer []byte `json:"extra_buffer"` - // ChatRoomType int `json:"chat_room_type"` } func (c *ContactV4) Wrap() *Contact { diff --git a/internal/model/media.go b/internal/model/media.go index cd03bfb..be99961 100644 --- a/internal/model/media.go +++ b/internal/model/media.go @@ -10,6 +10,7 @@ type Media struct { Path string `json:"path"` Name string `json:"name"` Size int64 `json:"size"` + Data []byte `json:"data"` // for voice ModifyTime int64 `json:"modifyTime"` } diff --git a/internal/model/mediamessage.go b/internal/model/mediamessage.go index b8f7254..c462f36 100644 --- a/internal/model/mediamessage.go +++ b/internal/model/mediamessage.go @@ -283,9 +283,34 @@ type FinderMegaVideo struct { } type SysMsg struct { - SysMsgTemplate SysMsgTemplate `xml:"sysmsgtemplate"` + Type string `xml:"type,attr"` + DelChatRoomMember *DelChatRoomMember `xml:"delchatroommember,omitempty"` + SysMsgTemplate *SysMsgTemplate `xml:"sysmsgtemplate,omitempty"` } +// 第一种消息类型:删除群成员/二维码邀请 +type DelChatRoomMember struct { + Plain string `xml:"plain"` + Text string `xml:"text"` + Link QRLink `xml:"link"` +} + +type QRLink struct { + Scene string `xml:"scene"` + Text string `xml:"text"` + MemberList QRMemberList `xml:"memberlist"` + QRCode string `xml:"qrcode"` +} + +type QRMemberList struct { + Usernames []UsernameItem `xml:"username"` +} + +type UsernameItem struct { + Value string `xml:",chardata"` +} + +// 第二种消息类型:系统消息模板 type SysMsgTemplate struct { ContentTemplate ContentTemplate `xml:"content_template"` } @@ -305,7 +330,8 @@ type Link struct { Name string `xml:"name,attr"` Type string `xml:"type,attr"` MemberList MemberList `xml:"memberlist"` - Separator string `xml:"separator"` + Separator string `xml:"separator,omitempty"` + Title string `xml:"title,omitempty"` } type MemberList struct { @@ -318,6 +344,24 @@ type Member struct { } func (s *SysMsg) String() string { + if s.Type == "delchatroommember" { + return s.DelChatRoomMemberString() + } + return s.SysMsgTemplateString() +} + +func (s *SysMsg) DelChatRoomMemberString() string { + if s.DelChatRoomMember == nil { + return "" + } + return s.DelChatRoomMember.Plain +} + +func (s *SysMsg) SysMsgTemplateString() string { + if s.SysMsgTemplate == nil { + return "" + } + template := s.SysMsgTemplate.ContentTemplate.Template links := s.SysMsgTemplate.ContentTemplate.LinkList.Links @@ -354,7 +398,11 @@ func (s *SysMsg) String() string { // 可以根据需要添加其他链接类型的处理逻辑 default: - replacement = "" + if link.Title != "" { + replacement = link.Title + } else { + replacement = "" + } } // 将占位符名称和替换内容存入映射 diff --git a/internal/model/message.go b/internal/model/message.go index ae61c59..b8b6b10 100644 --- a/internal/model/message.go +++ b/internal/model/message.go @@ -55,6 +55,8 @@ func (m *Message) ParseMediaInfo(data string) error { if Debug { m.SysMsg = &sysMsg } + m.Sender = "系统消息" + m.SenderName = "" m.Content = sysMsg.String() return nil } @@ -188,13 +190,8 @@ func (m *Message) PlainText(showChatRoom bool, host string) string { buf := strings.Builder{} sender := m.Sender - switch { - case m.Type == 10000: - sender = "系统消息" - case m.IsSelf: + if m.IsSelf { sender = "我" - default: - sender = m.Sender } if m.SenderName != "" { buf.WriteString(m.SenderName) @@ -235,6 +232,9 @@ func (m *Message) PlainTextContent() string { case 3: return fmt.Sprintf("![图片](http://%s/image/%s)", 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 "[名片]" diff --git a/internal/model/message_v3.go b/internal/model/message_v3.go index c57b351..1103d53 100644 --- a/internal/model/message_v3.go +++ b/internal/model/message_v3.go @@ -1,6 +1,7 @@ package model import ( + "fmt" "path/filepath" "strings" "time" @@ -39,6 +40,7 @@ import ( // BytesTrans BLOB // ) type MessageV3 struct { + MsgSvrID int64 `json:"MsgSvrID"` // 消息 ID Sequence int64 `json:"Sequence"` // 消息序号,10位时间戳 + 3位序号 CreateTime int64 `json:"CreateTime"` // 消息创建时间,10位时间戳 StrTalker string `json:"StrTalker"` // 聊天对象,微信 ID or 群 ID @@ -77,6 +79,11 @@ func (m *MessageV3) Wrap() *Message { _m.ParseMediaInfo(_m.Content) + // 语音消息 + if _m.Type == 34 { + _m.Contents["voice"] = fmt.Sprint(m.MsgSvrID) + } + if len(m.BytesExtra) != 0 { if bytesExtra := ParseBytesExtra(m.BytesExtra); bytesExtra != nil { if _m.IsChatRoom { diff --git a/internal/model/message_v4.go b/internal/model/message_v4.go index efc80fd..edf271e 100644 --- a/internal/model/message_v4.go +++ b/internal/model/message_v4.go @@ -2,6 +2,7 @@ package model import ( "bytes" + "fmt" "strings" "time" @@ -31,6 +32,7 @@ import ( // ) type MessageV4 struct { SortSeq int64 `json:"sort_seq"` // 消息序号,10位时间戳 + 3位序号 + ServerID int64 `json:"server_id"` // 消息 ID,用于关联 voice LocalType int64 `json:"local_type"` // 消息类型 UserName string `json:"user_name"` // 发送人,通过 Join Name2Id 表获得 CreateTime int64 `json:"create_time"` // 消息创建时间,10位时间戳 @@ -74,6 +76,11 @@ func (m *MessageV4) Wrap(talker string) *Message { _m.ParseMediaInfo(content) + // 语音消息 + if _m.Type == 34 { + _m.Contents["voice"] = fmt.Sprint(m.ServerID) + } + if len(m.PackedInfoData) != 0 { if packedInfo := ParsePackedInfo(m.PackedInfoData); packedInfo != nil { // FIXME 尝试解决 v4 版本 xml 数据无法匹配到 hardlink 记录的问题 diff --git a/internal/wechatdb/datasource/v4/datasource.go b/internal/wechatdb/datasource/v4/datasource.go index b45206a..589cfdd 100644 --- a/internal/wechatdb/datasource/v4/datasource.go +++ b/internal/wechatdb/datasource/v4/datasource.go @@ -23,6 +23,7 @@ const ( ContactFilePattern = "^contact\\.db$" SessionFilePattern = "^session\\.db$" MediaFilePattern = "^hardlink\\.db$" + VoiceFilePattern = "^media_([0-9]?[0-9])?\\.db$" ) // MessageDBInfo 存储消息数据库的信息 @@ -38,6 +39,7 @@ type DataSource struct { contactDb *sql.DB sessionDb *sql.DB mediaDb *sql.DB + voiceDb []*sql.DB // 消息数据库信息 messageFiles []MessageDBInfo @@ -47,6 +49,7 @@ func New(path string) (*DataSource, error) { ds := &DataSource{ path: path, messageDbs: make(map[string]*sql.DB), + voiceDb: make([]*sql.DB, 0), messageFiles: make([]MessageDBInfo, 0), } @@ -62,6 +65,9 @@ func New(path string) (*DataSource, error) { if err := ds.initMediaDb(path); err != nil { return nil, errors.DBInitFailed(err) } + if err := ds.initVoiceDb(path); err != nil { + return nil, errors.DBInitFailed(err) + } return ds, nil } @@ -173,6 +179,24 @@ func (ds *DataSource) initMediaDb(path string) error { return nil } +func (ds *DataSource) initVoiceDb(path string) error { + files, err := util.FindFilesWithPatterns(path, VoiceFilePattern, true) + if err != nil { + return errors.DBFileNotFound(path, VoiceFilePattern, err) + } + if len(files) == 0 { + return errors.DBFileNotFound(path, VoiceFilePattern, nil) + } + for _, file := range files { + db, err := sql.Open("sqlite3", file) + if err != nil { + return errors.DBConnectFailed(files[0], err) + } + ds.voiceDb = append(ds.voiceDb, db) + } + return nil +} + // getDBInfosForTimeRange 获取时间范围内的数据库信息 func (ds *DataSource) getDBInfosForTimeRange(startTime, endTime time.Time) []MessageDBInfo { var dbs []MessageDBInfo @@ -188,6 +212,7 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T if talker == "" { return nil, errors.ErrTalkerEmpty } + log.Debug().Msg(talker) // 找到时间范围内的数据库文件 dbInfos := ds.getDBInfosForTimeRange(startTime, endTime) @@ -215,7 +240,7 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T continue } - messages, err := ds.getMessagesFromDB(ctx, db, dbInfo, startTime, endTime, talker) + messages, err := ds.getMessagesFromDB(ctx, db, startTime, endTime, talker) if err != nil { log.Err(err).Msgf("从数据库 %s 获取消息失败", dbInfo.FilePath) continue @@ -260,12 +285,26 @@ func (ds *DataSource) getMessagesSingleFile(ctx context.Context, dbInfo MessageD talkerMd5 := hex.EncodeToString(_talkerMd5Bytes[:]) tableName := "Msg_" + talkerMd5 + // 检查表是否存在 + var exists bool + err := db.QueryRowContext(ctx, + "SELECT 1 FROM sqlite_master WHERE type='table' AND name=?", + tableName).Scan(&exists) + + if err != nil { + if err == sql.ErrNoRows { + // 表不存在,返回空结果 + return []*model.Message{}, nil + } + return nil, errors.QueryFailed("", err) + } + // 构建查询条件 conditions := []string{"create_time >= ? AND create_time <= ?"} args := []interface{}{startTime.Unix(), endTime.Unix()} query := fmt.Sprintf(` - SELECT m.sort_seq, m.local_type, n.user_name, m.create_time, m.message_content, m.packed_info_data, m.status + SELECT m.sort_seq, m.server_id, m.local_type, n.user_name, m.create_time, m.message_content, m.packed_info_data, m.status FROM %s m LEFT JOIN Name2Id n ON m.real_sender_id = n.rowid WHERE %s @@ -293,6 +332,7 @@ func (ds *DataSource) getMessagesSingleFile(ctx context.Context, dbInfo MessageD var msg model.MessageV4 err := rows.Scan( &msg.SortSeq, + &msg.ServerID, &msg.LocalType, &msg.UserName, &msg.CreateTime, @@ -311,7 +351,7 @@ func (ds *DataSource) getMessagesSingleFile(ctx context.Context, dbInfo MessageD } // getMessagesFromDB 从数据库获取消息 -func (ds *DataSource) getMessagesFromDB(ctx context.Context, db *sql.DB, dbInfo MessageDBInfo, startTime, endTime time.Time, talker string) ([]*model.Message, error) { +func (ds *DataSource) getMessagesFromDB(ctx context.Context, db *sql.DB, startTime, endTime time.Time, talker string) ([]*model.Message, error) { // 构建表名 _talkerMd5Bytes := md5.Sum([]byte(talker)) talkerMd5 := hex.EncodeToString(_talkerMd5Bytes[:]) @@ -336,7 +376,7 @@ func (ds *DataSource) getMessagesFromDB(ctx context.Context, db *sql.DB, dbInfo args := []interface{}{startTime.Unix(), endTime.Unix()} query := fmt.Sprintf(` - SELECT m.sort_seq, m.local_type, n.user_name, m.create_time, m.message_content, m.packed_info_data, m.status + SELECT m.sort_seq, m.server_id, m.local_type, n.user_name, m.create_time, m.message_content, m.packed_info_data, m.status FROM %s m LEFT JOIN Name2Id n ON m.real_sender_id = n.rowid WHERE %s @@ -361,6 +401,7 @@ func (ds *DataSource) getMessagesFromDB(ctx context.Context, db *sql.DB, dbInfo var msg model.MessageV4 err := rows.Scan( &msg.SortSeq, + &msg.ServerID, &msg.LocalType, &msg.UserName, &msg.CreateTime, @@ -605,10 +646,6 @@ func (ds *DataSource) GetMedia(ctx context.Context, _type string, key string) (* return nil, errors.ErrKeyEmpty } - if len(key) != 32 { - return nil, errors.ErrKeyLengthMust32 - } - var table string switch _type { case "image": @@ -617,6 +654,8 @@ func (ds *DataSource) GetMedia(ctx context.Context, _type string, key string) (* table = "video_hardlink_info_v3" case "file": table = "file_hardlink_info_v3" + case "voice": + return ds.GetVoice(ctx, key) default: return nil, errors.MediaTypeUnsupported(_type) } @@ -675,6 +714,46 @@ func (ds *DataSource) GetMedia(ctx context.Context, _type string, key string) (* return media, nil } +func (ds *DataSource) GetVoice(ctx context.Context, key string) (*model.Media, error) { + if key == "" { + return nil, errors.ErrKeyEmpty + } + + query := ` + SELECT voice_data + FROM VoiceInfo + WHERE svr_id = ? + ` + args := []interface{}{key} + + for _, db := range ds.voiceDb { + rows, err := db.QueryContext(ctx, query, args...) + if err != nil { + return nil, errors.QueryFailed(query, err) + } + defer rows.Close() + + for rows.Next() { + var voiceData []byte + err := rows.Scan( + &voiceData, + ) + if err != nil { + return nil, errors.ScanRowFailed(err) + } + if len(voiceData) > 0 { + return &model.Media{ + Type: "voice", + Key: key, + Data: voiceData, + }, nil + } + } + } + + return nil, errors.ErrMediaNotFound +} + func (ds *DataSource) Close() error { var errs []error diff --git a/internal/wechatdb/datasource/windowsv3/datasource.go b/internal/wechatdb/datasource/windowsv3/datasource.go index 8068e9b..41367ab 100644 --- a/internal/wechatdb/datasource/windowsv3/datasource.go +++ b/internal/wechatdb/datasource/windowsv3/datasource.go @@ -23,6 +23,7 @@ const ( ImageFilePattern = "^HardLinkImage\\.db$" VideoFilePattern = "^HardLinkVideo\\.db$" FileFilePattern = "^HardLinkFile\\.db$" + VoiceFilePattern = "^MediaMSG([0-9])?\\.db$" ) // MessageDBInfo 保存消息数据库的信息 @@ -46,6 +47,7 @@ type DataSource struct { imageDb *sql.DB videoDb *sql.DB fileDb *sql.DB + voiceDb []*sql.DB } // New 创建一个新的 WindowsV3DataSource @@ -53,6 +55,7 @@ func New(path string) (*DataSource, error) { ds := &DataSource{ messageFiles: make([]MessageDBInfo, 0), messageDbs: make(map[string]*sql.DB), + voiceDb: make([]*sql.DB, 0), } // 初始化消息数据库 @@ -69,6 +72,10 @@ func New(path string) (*DataSource, error) { return nil, errors.DBInitFailed(err) } + if err := ds.initVoiceDb(path); err != nil { + return nil, errors.DBInitFailed(err) + } + return ds, nil } @@ -238,6 +245,24 @@ func (ds *DataSource) initMediaDb(path string) error { return nil } +func (ds *DataSource) initVoiceDb(path string) error { + files, err := util.FindFilesWithPatterns(path, VoiceFilePattern, true) + if err != nil { + return errors.DBFileNotFound(path, VoiceFilePattern, err) + } + if len(files) == 0 { + return errors.DBFileNotFound(path, VoiceFilePattern, nil) + } + for _, file := range files { + db, err := sql.Open("sqlite3", file) + if err != nil { + return errors.DBConnectFailed(files[0], err) + } + ds.voiceDb = append(ds.voiceDb, db) + } + return nil +} + // getDBInfosForTimeRange 获取时间范围内的数据库信息 func (ds *DataSource) getDBInfosForTimeRange(startTime, endTime time.Time) []MessageDBInfo { var dbs []MessageDBInfo @@ -293,7 +318,7 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T } query := fmt.Sprintf(` - SELECT Sequence, CreateTime, StrTalker, IsSender, + SELECT MsgSvrID, Sequence, CreateTime, StrTalker, IsSender, Type, SubType, StrContent, CompressContent, BytesExtra FROM MSG WHERE %s @@ -314,6 +339,7 @@ func (ds *DataSource) GetMessages(ctx context.Context, startTime, endTime time.T var bytesExtra []byte err := rows.Scan( + &msg.MsgSvrID, &msg.Sequence, &msg.CreateTime, &msg.StrTalker, @@ -377,7 +403,7 @@ func (ds *DataSource) getMessagesSingleFile(ctx context.Context, dbInfo MessageD } } query := fmt.Sprintf(` - SELECT Sequence, CreateTime, StrTalker, IsSender, + SELECT MsgSvrID, Sequence, CreateTime, StrTalker, IsSender, Type, SubType, StrContent, CompressContent, BytesExtra FROM MSG WHERE %s @@ -406,6 +432,7 @@ func (ds *DataSource) getMessagesSingleFile(ctx context.Context, dbInfo MessageD var compressContent []byte var bytesExtra []byte err := rows.Scan( + &msg.MsgSvrID, &msg.Sequence, &msg.CreateTime, &msg.StrTalker, @@ -652,6 +679,10 @@ func (ds *DataSource) GetMedia(ctx context.Context, _type string, key string) (* return nil, errors.ErrKeyEmpty } + if _type == "voice" { + return ds.GetVoice(ctx, key) + } + md5key, err := hex.DecodeString(key) if err != nil { return nil, errors.DecodeKeyFailed(err) @@ -725,6 +756,46 @@ func (ds *DataSource) GetMedia(ctx context.Context, _type string, key string) (* return media, nil } +func (ds *DataSource) GetVoice(ctx context.Context, key string) (*model.Media, error) { + if key == "" { + return nil, errors.ErrKeyEmpty + } + + query := ` + SELECT Buf + FROM Media + WHERE Reserved0 = ? + ` + args := []interface{}{key} + + for _, db := range ds.voiceDb { + rows, err := db.QueryContext(ctx, query, args...) + if err != nil { + return nil, errors.QueryFailed(query, err) + } + defer rows.Close() + + for rows.Next() { + var voiceData []byte + err := rows.Scan( + &voiceData, + ) + if err != nil { + return nil, errors.ScanRowFailed(err) + } + if len(voiceData) > 0 { + return &model.Media{ + Type: "voice", + Key: key, + Data: voiceData, + }, nil + } + } + } + + return nil, errors.ErrMediaNotFound +} + // Close 实现 DataSource 接口的 Close 方法 func (ds *DataSource) Close() error { var errs []error diff --git a/pkg/util/silk/silk.go b/pkg/util/silk/silk.go new file mode 100644 index 0000000..6fc14bb --- /dev/null +++ b/pkg/util/silk/silk.go @@ -0,0 +1,36 @@ +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 +}