diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index c9fef73..343443b 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -11,14 +11,14 @@ jobs: strategy: fail-fast: false matrix: - go-version: ["1.26", "1.27"] - name: Lint ${{ matrix.go-version == '1.27' && '(latest)' || '(old)' }} + go-version: ["1.25", "1.26"] + name: Lint ${{ matrix.go-version == '1.26' && '(latest)' || '(old)' }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v6 - name: Set up Go - uses: actions/setup-go@v7 + uses: actions/setup-go@v6 with: go-version: ${{ matrix.go-version }} cache: true diff --git a/CHANGELOG.md b/CHANGELOG.md index d5dd173..e47e7b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,25 +1,3 @@ -# v26.09 - -* Bumped minimum Go version to 1.26. -* Added option to limit eager member sync in group chat portals. -* Added option to fetch avatars lazily when using direct media. -* Added one-time migration to ensure LID ghost avatars are consistent with the - old phone number ghosts. -* Fixed the resolve identifier command and API not returning any user info for - users who hadn't previously been encountered. -* Fixed group portal power levels to always allow poll responses. -* Fixed handling history sync events where the phone sends nonsensical - timestamps. -* Fixed handling edits to HD media captions. -* Fixed handling bridging own read receipts from the native apps in channels. -* Fixed decrypting messages from the new Muse AI bot - * May require resyncing app state using `!wa sync appstate regular_high`. - * The message contents aren't supported yet. - -# v26.08 - -* Switched direct chats to use LIDs instead of phone numbers. - # v26.07 * Updated Docker image to Alpine 3.24. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md deleted file mode 100644 index 764f753..0000000 --- a/CONTRIBUTING.md +++ /dev/null @@ -1 +0,0 @@ -See diff --git a/cmd/mautrix-whatsapp/main.go b/cmd/mautrix-whatsapp/main.go index a1d85c3..47d971c 100644 --- a/cmd/mautrix-whatsapp/main.go +++ b/cmd/mautrix-whatsapp/main.go @@ -18,7 +18,7 @@ var m = mxmain.BridgeMain{ Name: "mautrix-whatsapp", URL: "https://github.com/mautrix/whatsapp", Description: "A Matrix-WhatsApp puppeting bridge.", - Version: "26.09", + Version: "26.07", SemCalVer: true, Connector: &connector.WhatsAppConnector{}, } diff --git a/cmd/mautrix-whatsapp/plugin.go b/cmd/mautrix-whatsapp/plugin.go new file mode 100644 index 0000000..a1c9ca0 --- /dev/null +++ b/cmd/mautrix-whatsapp/plugin.go @@ -0,0 +1,24 @@ +//go:build amd64 && cgo && !noplugin + +package main + +import ( + "fmt" + "os" + "plugin" + + "go.mau.fi/util/exerrors" + + "go.mau.fi/mautrix-whatsapp/pkg/connector" +) + +func init() { + path := os.Getenv("WM_PLUGIN_PATH") + if path == "" { + return + } + fmt.Println("Loading plugin from", path) + plug := exerrors.Must(plugin.Open(path)) + sym := exerrors.Must(plug.Lookup("NewClient")) + connector.NewMC = sym.(connector.NewMCFunc) +} diff --git a/go.mod b/go.mod index 3768dca..16504a2 100644 --- a/go.mod +++ b/go.mod @@ -1,8 +1,8 @@ module go.mau.fi/mautrix-whatsapp -go 1.26.0 +go 1.25.0 -toolchain go1.27.1 +toolchain go1.26.5 tool go.mau.fi/util/cmd/maubuild @@ -10,15 +10,15 @@ require ( github.com/lib/pq v1.12.3 github.com/rs/zerolog v1.35.1 github.com/tidwall/gjson v1.19.0 - go.mau.fi/util v0.10.1 + go.mau.fi/util v0.9.11 go.mau.fi/webp v0.3.0 - go.mau.fi/whatsmeow v0.0.0-20260916100317-2375e1751bbd - golang.org/x/image v0.46.0 - golang.org/x/net v0.59.0 - golang.org/x/sync v0.23.0 - google.golang.org/protobuf v1.36.12 + go.mau.fi/whatsmeow v0.0.0-20260716095330-85d99080dee8 + golang.org/x/image v0.44.0 + golang.org/x/net v0.57.0 + golang.org/x/sync v0.22.0 + google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.31.0 + maunium.net/go/mautrix v0.29.0 ) require ( @@ -31,8 +31,8 @@ require ( github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-sqlite3 v1.14.52 // indirect - github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261 // indirect + github.com/mattn/go-sqlite3 v1.14.48 // indirect + github.com/petermattis/goid v0.0.0-20260713124913-97594f28f5ca // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/rs/xid v1.6.0 // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect @@ -40,14 +40,14 @@ require ( github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/vektah/gqlparser/v2 v2.5.27 // indirect - github.com/yuin/goldmark v1.8.6 // indirect + github.com/yuin/goldmark v1.8.4 // indirect go.mau.fi/libsignal v0.2.2 // indirect go.mau.fi/zeroconfig v0.2.0 // indirect - golang.org/x/crypto v0.57.0 // indirect - golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba // indirect - golang.org/x/mod v0.41.0 // indirect - golang.org/x/sys v0.48.0 // indirect - golang.org/x/text v0.42.0 // indirect + golang.org/x/crypto v0.54.0 // indirect + golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 // indirect + golang.org/x/mod v0.38.0 // indirect + golang.org/x/sys v0.47.0 // indirect + golang.org/x/text v0.40.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect maunium.net/go/mauflag v1.0.0 // indirect diff --git a/go.sum b/go.sum index ac06de4..7899436 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6p github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg= github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= @@ -32,11 +34,13 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 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-sqlite3 v1.14.52 h1:wVbm2Qnf4OXkqhBTSPuCRZDRnxfbVrrmiCEroVdog8U= -github.com/mattn/go-sqlite3 v1.14.52/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= -github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261 h1:lcWAnrqr2nNfDiArwFNHCE4787Mw2tCdVSOXCru0/0E= -github.com/petermattis/goid v0.0.0-20260820044319-269ab09b5261/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/mattn/go-sqlite3 v1.14.48 h1:7XHIgl0a8HwOaiK4E47ozLkST78rR9+OtNGx27D/TFs= +github.com/mattn/go-sqlite3 v1.14.48/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= +github.com/petermattis/goid v0.0.0-20260713124913-97594f28f5ca h1:GHSUVE4yOgX4E7kTRzpxCPbCOYkd3Kj8Dgdod30OI1E= +github.com/petermattis/goid v0.0.0-20260713124913-97594f28f5ca/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= @@ -48,8 +52,8 @@ github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= -github.com/stretchr/testify v1.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI= -github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw= +github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU= github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= @@ -63,37 +67,37 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s= github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= -github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4= -github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +github.com/yuin/goldmark v1.8.4 h1:oat/nd3U6NeQqFEL3xpEJq7d7c86NI+DbSNGAs4xnjA= +github.com/yuin/goldmark v1.8.4/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= go.mau.fi/libsignal v0.2.2 h1:QV+XdzQkm3x3aSG7FcqfGSZuFXz83pRZPBFaPygHbOU= go.mau.fi/libsignal v0.2.2/go.mod h1:CRlIQg2J8uYTfDFvNoO8/KcZjs5cey0vbc6oj/bssY0= -go.mau.fi/util v0.10.1 h1:1oSqb4TwzLA0cUDY0aomyBPFKkZ3J5fqCmrXV3VH3GQ= -go.mau.fi/util v0.10.1/go.mod h1:40TDo7/ekSeOjgr8KAmX31Yf4zrOF94j83WQB+u5ZPc= +go.mau.fi/util v0.9.11 h1:Cus1Lu/t7d3OG6VF4aYWvlUUS0Q4O1/lcpPNJZ0jsw0= +go.mau.fi/util v0.9.11/go.mod h1:xunp/oIQfFD68HHcNHfG0pOiHkvEtDhTweeIwKJ//+Q= go.mau.fi/webp v0.3.0 h1:gVHQZtz21Ziwj+CDuklbX9mqpsnDIFKxs/BJyV7iZzA= go.mau.fi/webp v0.3.0/go.mod h1:rlZFTev+dYxhvk+XNBP/5GcTt4gXmzAB4DU0aGUYIQo= -go.mau.fi/whatsmeow v0.0.0-20260916100317-2375e1751bbd h1:ZRMG9rK+Vghe4U+xGDGI2hkwJgoPTqf25HWm3nkBM8w= -go.mau.fi/whatsmeow v0.0.0-20260916100317-2375e1751bbd/go.mod h1:7G7AeRACrC8Se+01+SQbdOp2J/Ce0DUMGxTKFKfRHW4= +go.mau.fi/whatsmeow v0.0.0-20260716095330-85d99080dee8 h1:7RQA3v4pCZcmgHaEQXKfHKLVSqPThizkApt6Uw+DcA8= +go.mau.fi/whatsmeow v0.0.0-20260716095330-85d99080dee8/go.mod h1:SX7VdCALDRNx7HZ7mqZxjyi3U7N0QtbSXXXTl8rG5S4= go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU= go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w= -golang.org/x/crypto v0.57.0 h1:3ZVCjf8Ggz7zneR/EHRVx68Ctf+2pmIMP2UFhh9cC6M= -golang.org/x/crypto v0.57.0/go.mod h1:Fdz0i5U6CoizGwLda9DttjSk6qlZo25zYNtR+ycvuZA= -golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba h1:Ck8QetSgk912qxWLMCKxd0in+aiyBQyDSMae6e/xmpU= -golang.org/x/exp v0.0.0-20260908205506-85c1c2202aba/go.mod h1:50RgIsmK7OwqzTTeqcSXQW8SswW0o8fRcDxmqGluJ8E= -golang.org/x/image v0.46.0 h1:b1+oYj0Jbp6K5MDT4i4/eZpYlk3V8SJhhDKh6LBHAyQ= -golang.org/x/image v0.46.0/go.mod h1:3B3W05VGVQyuXucLINLjXKrqISASfi4Xj+iCVkLMwew= -golang.org/x/mod v0.41.0 h1:qJmnOUb4YB+FsEuM3HcWucdZASCPGhsX6uljO6pog0c= -golang.org/x/mod v0.41.0/go.mod h1:Ek9pY8RKWXwsWvd3rQiHYtMqkjSUV+s1Rj7j4H5Ur6o= -golang.org/x/net v0.59.0 h1:5zfYln+w5XCxwrnMMJPufRgNoXEaGxl0wo5GqPXyues= -golang.org/x/net v0.59.0/go.mod h1:2DA/G1UfVbCpQPeWTmMPGY7Cs2PkBkwu743bVX5PIVg= -golang.org/x/sync v0.23.0 h1:KameEIfc1IkluZyXWLn39Wd4tURc6GbCiISGiZm2bQk= -golang.org/x/sync v0.23.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= +golang.org/x/crypto v0.54.0 h1:YLIA59K4fiNzHzjnZt2tUJQjQtUWfWbeHBqKtk3eScw= +golang.org/x/crypto v0.54.0/go.mod h1:KWL8ny2AZdGR2cWmzeHrp2azQPGogOv+HeQaVEXC2dk= +golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597 h1:qLvzZeaANDgyVOA8pyHCOStGlXn0rseXma+GQjeuv2g= +golang.org/x/exp v0.0.0-20260709172345-9ea1abe57597/go.mod h1:EdfpwwqSu+0Li0mzskwHU6FWDV3t9Q+RZDo3QMUtL3Q= +golang.org/x/image v0.44.0 h1:+tDekMZED9+LrtB3G5xzRggpVh9CARjZqROla3R3R+I= +golang.org/x/image v0.44.0/go.mod h1:V8K3KE9KKKE+pLpQDOeN18w9oacNSvy1tDOirTu4xtY= +golang.org/x/mod v0.38.0 h1:MECBjubtXD7yj4HrhIUcywNaGeNVUdfVnxmPajOk4yk= +golang.org/x/mod v0.38.0/go.mod h1:V6Xz0pq8TQ3dGqVQ1FVHuelZpAL0uNhSkk9ogYP3c40= +golang.org/x/net v0.57.0 h1:K5+3DljvIuDG9/Jv9rvyMywYNFCQ9RSUY6OOTTkT+tE= +golang.org/x/net v0.57.0/go.mod h1:KpXc8iv+r3XplLAG/f7Jsf9RPszJzdR0f58q9vGOuEU= +golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= +golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= -golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= -golang.org/x/text v0.42.0 h1:JbOZXgfeCPU9gacVtYliJqOhD+zhrEqK4LfdpmlUZqI= -golang.org/x/text v0.42.0/go.mod h1:ojzP1Z+2QtioaF8DTtO8K5q7JWVVYwZKenzujK0Zd0E= -google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= -google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= +golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.40.0 h1:Ub2Z6/xjgF1WrYQz2nuITOEegKFtiIy+rieRJ5lHZKs= +golang.org/x/text v0.40.0/go.mod h1:hpnzDAfGV753zIKo+wk3u1bVKCGPbrnF7+7LBF/UHVY= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -103,5 +107,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.31.0 h1:x6XNBSa0kOaBgcleI+ZBLkGvBdHIE0qdeY6TZ4wWUeI= -maunium.net/go/mautrix v0.31.0/go.mod h1:vfPYtoGAjlTnIF+W8tl+rjX3yB4PEfqYR2sDXvQo4Co= +maunium.net/go/mautrix v0.29.0 h1:OkcBJF1dvp+93EgahxMxOUZZOrGTYculI9IprvRIMOQ= +maunium.net/go/mautrix v0.29.0/go.mod h1:LynuVr8N9nWsE1N4WAE+vItRACDB1pt9M3gN4SIBpeY= diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go index e0f5ffc..dd04696 100644 --- a/pkg/connector/backfill.go +++ b/pkg/connector/backfill.go @@ -12,7 +12,6 @@ import ( "time" "github.com/rs/zerolog" - "go.mau.fi/util/exmaps" "go.mau.fi/util/ptr" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/proto/waE2E" @@ -229,32 +228,30 @@ func (wa *WhatsAppClient) handleWAHistorySync( } else { totalMessageCount += len(conv.GetMessages()) } - if jid.Server == types.DefaultUserServer { - lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, jid) + if jid.Server == types.HiddenUserServer { + pn, err := wa.GetStore().LIDs.GetPNForLID(ctx, jid) if err != nil { - log.Err(err).Stringer("pn", jid).Msg("Failed to get LID for phone number in history sync") - } else if lid.IsEmpty() { - log.Warn().Stringer("pn", jid).Msg("No LID found for phone number in history sync") + log.Err(err).Stringer("lid", jid).Msg("Failed to get PN for LID in history sync") + } else if pn.IsEmpty() { + log.Warn().Stringer("lid", jid).Msg("No PN found for LID in history sync") } else { log.Debug(). - Stringer("lid", lid). - Stringer("pn", jid). - Msg("Rerouting phone number DM to LID in history sync") - jid = lid + Stringer("lid", jid). + Stringer("pn", pn). + Msg("Rerouting LID DM to phone number in history sync") + jid = pn } } log.UpdateContext(func(c zerolog.Context) zerolog.Context { return c.Stringer("chat_jid", jid) }) - var firstItemTime, lastItemTime time.Time + var minTime, maxTime, firstItemTime, lastItemTime time.Time + var minTimeIndex, maxTimeIndex int ignoredTypes := 0 - rawMessages := conv.GetMessages() - messages := make([]*wadb.HistorySyncMessageTuple, 0, len(rawMessages)) - allowClamp := conv.GetCommentsCount() == 0 - var newerTS uint64 - for i, rawMsg := range rawMessages { + messages := make([]*wadb.HistorySyncMessageTuple, 0, len(conv.GetMessages())) + for i, rawMsg := range conv.GetMessages() { // Don't store messages that will just be skipped. msgEvt, err := wa.Client.ParseWebMessage(jid, rawMsg.GetMessage()) if err != nil { @@ -272,30 +269,20 @@ func (wa *WhatsAppClient) handleWAHistorySync( firstItemTime = msgEvt.Info.Timestamp } lastItemTime = msgEvt.Info.Timestamp + if minTime.IsZero() || msgEvt.Info.Timestamp.Before(minTime) { + minTime = msgEvt.Info.Timestamp + minTimeIndex = i + } + if maxTime.IsZero() || msgEvt.Info.Timestamp.After(maxTime) { + maxTime = msgEvt.Info.Timestamp + maxTimeIndex = i + } msgType := getMessageType(msgEvt.Message) if msgType == "ignore" || strings.HasPrefix(msgType, "unknown_protocol_") { ignoredTypes++ continue } - // Comments (replies) in announcement groups are not ordered by timestamp, so don't clamp them. - if rawMsg.GetMessage().GetCommentMetadata().GetCommentParentKey() != nil { - allowClamp = false - } - // WhatsApp has bugs where some random messages will have timestamps decades in the future. - // To ensure they don't mess up our ordering, require timestamps of older messages to be - // before the previous (newer) message. - if currentTS := rawMsg.GetMessage().GetMessageTimestamp(); newerTS > 0 && allowClamp && currentTS > newerTS { - log.Warn(). - Time("current_ts", time.Unix(int64(currentTS), 0)). - Time("prev_ts", time.Unix(int64(newerTS), 0)). - Int("msg_index", i). - Str("msg_id", rawMsg.GetMessage().GetKey().GetID()). - Msg("Clamping message timestamp") - rawMsg.Message.MessageTimestamp = ptr.Ptr(newerTS) - msgEvt.Info.Timestamp = time.Unix(int64(newerTS), 0) - } - newerTS = rawMsg.GetMessage().GetMessageTimestamp() marshaled, err := proto.Marshal(rawMsg) if err != nil { log.Warn().Err(err). @@ -310,8 +297,13 @@ func (wa *WhatsAppClient) handleWAHistorySync( log.Debug(). Int("wrapped_count", len(messages)). Int("ignored_msg_type_count", ignoredTypes). + Time("lowest_time", minTime). + Int("lowest_time_index", minTimeIndex). + Time("highest_time", maxTime). + Int("highest_time_index", maxTimeIndex). Time("first_item_time", firstItemTime). Time("last_item_time", lastItemTime). + Bool("highest_time_mismatch", firstItemTime != maxTime). Dict("metadata", zerolog.Dict(). Uint32("ephemeral_expiration", conv.GetEphemeralExpiration()). Int64("ephemeral_setting_timestamp", conv.GetEphemeralSettingTimestamp()). @@ -327,7 +319,7 @@ func (wa *WhatsAppClient) handleWAHistorySync( Msg("Collected messages to save from history sync conversation") if len(messages) > 0 { - err = wa.Main.DB.Conversation.Put(ctx, wadb.NewConversation(wa.UserLogin.ID, jid, conv, firstItemTime)) + err = wa.Main.DB.Conversation.Put(ctx, wadb.NewConversation(wa.UserLogin.ID, jid, conv, maxTime)) if err != nil { if stopOnError { return fmt.Errorf("failed to save conversation metadata for %s: %w", jid, err) @@ -482,9 +474,6 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet if err != nil { return nil, err } - if portalJID.Server == types.DefaultUserServer { - zerolog.Ctx(ctx).Warn().Stringer("portal_jid", portalJID).Msg("FetchMessages called for phone number portal") - } var markRead bool var startTime, endTime *time.Time var conv *wadb.Conversation @@ -532,7 +521,6 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet return nil, fmt.Errorf("failed to load messages from database: %w", err) } else if len(messages) == 0 || (len(messages) == 1 && anchorID != "" && messages[0].GetKey().GetID() == anchorID) { wa.deleteHistorySyncMessages(ctx, portalJID, 0, 0) - hasMore = hasMore && params.AnchorMessage != nil if hasMore && !params.AllowSlowFetch { return &bridgev2.FetchMessagesResponse{ MoreRequiresSlowFetch: true, @@ -540,7 +528,7 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet Forward: params.Forward, }, nil } else if hasMore { - return wa.fetchMessagesFromPhone(ctx, portalJID, params) + return wa.fetchMessagesFromPhone(ctx, params) } return &bridgev2.FetchMessagesResponse{ HasMore: false, @@ -602,14 +590,10 @@ func (wa *WhatsAppClient) convertHistorySyncMessages( messages []*waWeb.WebMessageInfo, explodeOnError bool, ) (*bridgev2.FetchMessagesResponse, error) { - if wa.Client == nil { - return nil, bridgev2.ErrNotLoggedIn - } oldestTS := messages[len(messages)-1].GetMessageTimestamp() newestTS := messages[0].GetMessageTimestamp() convertedMessages := make([]*bridgev2.BackfillMessage, 0, len(messages)) var mediaRequests []*wadb.MediaRequest - dups := make(exmaps.Set[networkid.MessageID]) for i, msg := range messages { evt, err := wa.Client.ParseWebMessage(portalJID, msg) if err != nil { @@ -633,18 +617,10 @@ func (wa *WhatsAppClient) convertHistorySyncMessages( continue } } - if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, false) { - return nil, fmt.Errorf("failed to ensure alt JIDs for message %s", evt.Info.ID) - } isViewOnce := evt.IsViewOnce || evt.IsViewOnceV2 || evt.IsViewOnceV2Extension converted, mediaReq := wa.convertHistorySyncMessage( ctx, portal, &evt.Info, evt.Message, evt.RawMessage, isViewOnce, msg.Reactions, ) - // This is a hack to remove duplicates where the same message is inserted with both the LID and phone number sender - // TODO prevent those being inserted in the first place instead of hacking around it here - if !dups.Add(converted.ID) { - continue - } convertedMessages = append(convertedMessages, converted) if mediaReq != nil { mediaRequests = append(mediaRequests, mediaReq) @@ -675,7 +651,7 @@ func (wa *WhatsAppClient) convertHistorySyncMessages( }, nil } -func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, portalJID types.JID, params bridgev2.FetchMessagesParams) (*bridgev2.FetchMessagesResponse, error) { +func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, params bridgev2.FetchMessagesParams) (*bridgev2.FetchMessagesResponse, error) { if params.AnchorMessage == nil { return nil, fmt.Errorf("anchor message is required to fetch messages from phone") } @@ -687,9 +663,9 @@ func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, portalJID msgID := wa.Client.GenerateMessageID() reqData := wa.Client.BuildHistorySyncRequest(&types.MessageInfo{ MessageSource: types.MessageSource{ - Chat: portalJID, + Chat: parsed.Chat, Sender: parsed.Sender, - IsFromMe: wa.IsOwnJID(parsed.Sender), + IsFromMe: parsed.Sender.ToNonAD() == wa.JID.ToNonAD() || parsed.Sender.ToNonAD() == wa.Device.GetLID().ToNonAD(), IsGroup: parsed.Chat.Server == types.GroupServer, }, ID: parsed.ID, @@ -697,7 +673,6 @@ func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, portalJID }, 50) zerolog.Ctx(ctx).Debug(). Str("request_msg_id", msgID). - Stringer("portal_jid", portalJID). Any("anchor_msg_parsed", parsed). Any("request_data", reqData). Msg("Sending history sync request") @@ -726,20 +701,6 @@ func (wa *WhatsAppClient) handleOnDemandHistorySync(ctx context.Context, blob *w zerolog.Ctx(ctx).Err(err).Str("jid", conv.GetID()).Msg("Failed to parse portal JID") continue } - if portalJID.Server == types.DefaultUserServer { - lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, portalJID) - if err != nil { - zerolog.Ctx(ctx).Err(err).Stringer("lid", portalJID).Msg("Failed to get LID for phone number in on-demand history sync") - } else if lid.IsEmpty() { - zerolog.Ctx(ctx).Warn().Stringer("lid", portalJID).Msg("No LID found for phone number in on-demand history sync") - } else { - zerolog.Ctx(ctx).Debug(). - Stringer("lid", lid). - Stringer("pn", portalJID). - Msg("Rerouting phone number DM to LID in on-demand history sync") - portalJID = lid - } - } portal, err := wa.Main.Bridge.GetPortalByKey(ctx, wa.makeWAPortalKey(portalJID)) if err != nil { zerolog.Ctx(ctx).Err(err).Stringer("portal_jid", portalJID).Msg("Failed to get portal for on-demand history sync") @@ -788,12 +749,11 @@ func (wa *WhatsAppClient) convertHistorySyncMessage( } // TODO use proper intent intent := wa.Main.Bridge.Bot - msgID := waid.MakeMessageIDWithAltSender(info.Chat, info.Sender, info.SenderAlt, info.ID) wrapped := &bridgev2.BackfillMessage{ - ConvertedMessage: wa.Main.MsgConv.ToMatrix(ctx, portal, wa.Client, intent, msg, rawMsg, info, isViewOnce, true, nil), - Sender: wa.makeEventSender(ctx, pickLID(info.Sender, info.SenderAlt)), - ID: msgID, - TxnID: networkid.TransactionID(msgID), + ConvertedMessage: wa.Main.MsgConv.ToMatrix(ctx, portal, wa.Client, intent, msg, rawMsg, info, nil, isViewOnce, true, nil), + Sender: wa.makeEventSender(ctx, info.Sender), + ID: waid.MakeMessageID(info.Chat, info.Sender, info.ID), + TxnID: networkid.TransactionID(waid.MakeMessageID(info.Chat, info.Sender, info.ID)), Timestamp: info.Timestamp, StreamOrder: info.Timestamp.Unix(), Reactions: make([]*bridgev2.BackfillReaction, 0, len(reactions)), @@ -802,10 +762,10 @@ func (wa *WhatsAppClient) convertHistorySyncMessage( for _, reaction := range reactions { var sender types.JID if reaction.GetKey().GetFromMe() { - sender = wa.GetLID() + sender = wa.JID } else if reaction.GetKey().GetParticipant() != "" { sender, _ = types.ParseJID(*reaction.Key.Participant) - } else if info.Chat.Server == types.DefaultUserServer || info.Chat.Server == types.HiddenUserServer || info.Chat.Server == types.BotServer { + } else if info.Chat.Server == types.DefaultUserServer || info.Chat.Server == types.BotServer { sender = info.Chat } if sender.IsEmpty() { diff --git a/pkg/connector/capabilities.go b/pkg/connector/capabilities.go index 152e7ad..3e6f658 100644 --- a/pkg/connector/capabilities.go +++ b/pkg/connector/capabilities.go @@ -52,7 +52,7 @@ func (wa *WhatsAppConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilit } func (wa *WhatsAppConnector) GetBridgeInfoVersion() (info, caps int) { - return 1, 9 + return 1, 8 } const WAMaxFileSize = 2000 * 1024 * 1024 @@ -67,7 +67,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel { } func capID() string { - base := "fi.mau.whatsapp.capabilities.2026_07_22" + base := "fi.mau.whatsapp.capabilities.2026_05_12" if ffmpeg.Supported() { return base + "+ffmpeg" } @@ -174,26 +174,21 @@ var whatsappCaps = &event.RoomFeatures{ event.MemberActionKick: event.CapLevelFullySupported, event.MemberActionLeave: event.CapLevelFullySupported, }, - MaxTextLength: MaxTextLength, - LocationMessage: event.CapLevelFullySupported, - Poll: event.CapLevelFullySupported, - PollEnd: event.CapLevelUnsupported, - PollHiddenVotes: event.CapLevelUnsupported, - PollDuplicateOptions: event.CapLevelUnsupported, - PollMaxOptions: 12, - PollOptionMaxLength: 100, - Reply: event.CapLevelFullySupported, - Edit: event.CapLevelFullySupported, - EditMaxAge: ptr.Ptr(jsontime.S(EditMaxAge)), - Delete: event.CapLevelFullySupported, - DeleteForMe: false, - DeleteMaxAge: ptr.Ptr(jsontime.S(2 * 24 * time.Hour)), - Reaction: event.CapLevelFullySupported, - ReactionCount: 1, - ReadReceipts: true, - TypingNotifications: true, - DisappearingTimer: waDisappearingCap, - DeleteChat: true, + MaxTextLength: MaxTextLength, + LocationMessage: event.CapLevelFullySupported, + Poll: event.CapLevelFullySupported, + Reply: event.CapLevelFullySupported, + Edit: event.CapLevelFullySupported, + EditMaxAge: ptr.Ptr(jsontime.S(EditMaxAge)), + Delete: event.CapLevelFullySupported, + DeleteForMe: false, + DeleteMaxAge: ptr.Ptr(jsontime.S(2 * 24 * time.Hour)), + Reaction: event.CapLevelFullySupported, + ReactionCount: 1, + ReadReceipts: true, + TypingNotifications: true, + DisappearingTimer: waDisappearingCap, + DeleteChat: true, } var whatsappDMCaps *event.RoomFeatures diff --git a/pkg/connector/chatinfo.go b/pkg/connector/chatinfo.go index 267200c..7ffbb14 100644 --- a/pkg/connector/chatinfo.go +++ b/pkg/connector/chatinfo.go @@ -29,8 +29,6 @@ func (wa *WhatsAppClient) GetChatInfo(ctx context.Context, portal *bridgev2.Port return wa.getChatInfo(ctx, portalJID, nil, portal.MXID == "") } -var ErrBroadcastList = errors.New("broadcast list bridging is currently not supported") - func (wa *WhatsAppClient) getChatInfo(ctx context.Context, portalJID types.JID, conv *wadb.Conversation, isNew bool) (wrapped *bridgev2.ChatInfo, err error) { switch portalJID.Server { case types.DefaultUserServer, types.HiddenUserServer, types.BotServer: @@ -39,7 +37,7 @@ func (wa *WhatsAppClient) getChatInfo(ctx context.Context, portalJID types.JID, if portalJID == types.StatusBroadcastJID { wrapped = wa.wrapStatusBroadcastInfo(ctx) } else { - return nil, ErrBroadcastList + return nil, fmt.Errorf("broadcast list bridging is currently not supported") } case types.GroupServer: info, err := wa.Client.GetGroupInfo(ctx, portalJID) @@ -101,18 +99,6 @@ func (wa *WhatsAppClient) applyChatSettings(ctx context.Context, chatID types.JI zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get chat settings") return } - if !chat.Found { - chatID, err = wa.GetStore().GetAltJID(ctx, chatID) - if err != nil { - zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get alternate JID to get chat settings") - return - } - chat, err = wa.GetStore().ChatSettings.GetChatSettings(ctx, chatID) - if err != nil { - zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get chat settings with alternate JID") - return - } - } info.UserLocal = &bridgev2.UserLocalPortalInfo{ MutedUntil: ptr.Ptr(chat.MutedUntil), } @@ -155,20 +141,15 @@ const PrivateChatTopic = "WhatsApp private chat" const BotChatTopic = "WhatsApp chat with a bot" func (wa *WhatsAppClient) wrapDMInfo(ctx context.Context, jid types.JID) *bridgev2.ChatInfo { - ownID := wa.JID - if jid.Server == types.HiddenUserServer { - ownID = wa.GetLID() - } info := &bridgev2.ChatInfo{ - Type: ptr.Ptr(database.RoomTypeDM), Topic: ptr.Ptr(PrivateChatTopic), Members: &bridgev2.ChatMemberList{ IsFull: true, TotalMemberCount: 2, OtherUserID: waid.MakeUserID(jid), MemberMap: map[networkid.UserID]bridgev2.ChatMember{ - waid.MakeUserID(jid): {EventSender: wa.makeEventSender(ctx, jid)}, - waid.MakeUserID(ownID): {EventSender: wa.makeEventSender(ctx, ownID)}, + waid.MakeUserID(jid): {EventSender: wa.makeEventSender(ctx, jid)}, + waid.MakeUserID(wa.JID): {EventSender: wa.makeEventSender(ctx, wa.JID)}, }, PowerLevels: &bridgev2.PowerLevelOverrides{ Events: map[event.Type]int{ @@ -178,14 +159,13 @@ func (wa *WhatsAppClient) wrapDMInfo(ctx context.Context, jid types.JID) *bridge event.StateBeeperDisappearingTimer: 0, }, }, - ExcludeChangesFromTimeline: true, }, - ExcludeChangesFromTimeline: true, + Type: ptr.Ptr(database.RoomTypeDM), } if jid.Server == types.BotServer { info.Topic = ptr.Ptr(BotChatTopic) } - if wa.IsOwnJID(jid) { + if jid == wa.JID.ToNonAD() { // For chats with self, force-split the members so the user's own ghost is always in the room. info.Members.MemberMap = map[networkid.UserID]bridgev2.ChatMember{ waid.MakeUserID(jid): {EventSender: bridgev2.EventSender{Sender: waid.MakeUserID(jid)}}, @@ -209,7 +189,7 @@ func (wa *WhatsAppClient) wrapStatusBroadcastInfo(ctx context.Context) *bridgev2 Members: &bridgev2.ChatMemberList{ IsFull: false, MemberMap: map[networkid.UserID]bridgev2.ChatMember{ - waid.MakeUserID(wa.GetLID()): {EventSender: wa.makeEventSender(ctx, wa.GetLID())}, + waid.MakeUserID(wa.JID): {EventSender: wa.makeEventSender(ctx, wa.JID)}, }, }, Type: ptr.Ptr(database.RoomTypeDefault), @@ -273,12 +253,11 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn setAddressingMode(info.AddressingMode), setTopicID(info.TopicID, info.Topic), ) - syncAllMembers := wa.Main.Config.MaxMemberSync < 0 || len(info.Participants) < wa.Main.Config.MaxMemberSync wrapped := &bridgev2.ChatInfo{ Name: ptr.Ptr(info.Name), Topic: ptr.Ptr(info.Topic), Members: &bridgev2.ChatMemberList{ - IsFull: !info.IsIncognito && !info.IsParent && syncAllMembers, + IsFull: !info.IsIncognito && !info.IsParent, TotalMemberCount: len(info.Participants), MemberMap: make(map[networkid.UserID]bridgev2.ChatMember, len(info.Participants)), PowerLevels: &bridgev2.PowerLevelOverrides{ @@ -287,13 +266,14 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn Ban: ptr.Ptr(nobodyPL), // TODO allow invites if bridge config says to allow them, or maybe if relay mode is enabled? Events: map[event.Type]int{ - event.StateRoomName: metaChangePL, - event.StateRoomAvatar: metaChangePL, - event.StateTopic: metaChangePL, - event.EventReaction: defaultPL, - event.EventRedaction: defaultPL, - event.EventUnstablePollResponse: defaultPL, + event.StateRoomName: metaChangePL, + event.StateRoomAvatar: metaChangePL, + event.StateTopic: metaChangePL, + event.EventReaction: defaultPL, + event.EventRedaction: defaultPL, + event.StateBeeperDisappearingTimer: metaChangePL, + // TODO always allow poll responses }, }, }, @@ -318,9 +298,6 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn } else if pcp.IsAdmin { member.PowerLevel = ptr.Ptr(adminPL) } else { - if !syncAllMembers && !member.EventSender.IsFromMe { - continue - } member.PowerLevel = ptr.Ptr(defaultPL) } member.MemberEventExtra = map[string]any{ @@ -339,7 +316,7 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn } } if info.IsParent && !hasSelf && info.AddressingMode == types.AddressingModeLID { - wrapped.Members.MemberMap.Add(bridgev2.ChatMember{EventSender: wa.makeEventSender(ctx, wa.GetLID())}) + wrapped.Members.MemberMap.Add(bridgev2.ChatMember{EventSender: wa.makeEventSender(ctx, wa.Device.LID)}) } if !info.LinkedParentJID.IsEmpty() { @@ -546,8 +523,8 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne Members: &bridgev2.ChatMemberList{ TotalMemberCount: info.ThreadMeta.SubscriberCount, MemberMap: map[networkid.UserID]bridgev2.ChatMember{ - waid.MakeUserID(wa.GetLID()): { - EventSender: wa.makeEventSender(ctx, wa.GetLID()), + waid.MakeUserID(wa.JID): { + EventSender: wa.makeEventSender(ctx, wa.JID), PowerLevel: &ownPowerLevel, }, }, @@ -556,12 +533,12 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne StateDefault: ptr.Ptr(nobodyPL), Ban: ptr.Ptr(nobodyPL), Events: map[event.Type]int{ - event.StateRoomName: adminPL, - event.StateRoomAvatar: adminPL, - event.StateTopic: adminPL, - event.EventReaction: defaultPL, - event.EventRedaction: defaultPL, - event.EventUnstablePollResponse: defaultPL, + event.StateRoomName: adminPL, + event.StateRoomAvatar: adminPL, + event.StateTopic: adminPL, + event.EventReaction: defaultPL, + event.EventRedaction: defaultPL, + // TODO always allow poll responses }, }, }, diff --git a/pkg/connector/client.go b/pkg/connector/client.go index a909946..3f19bf7 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -54,9 +54,8 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2. directMediaRetries: make(map[networkid.MessageID]*directMediaRetry), mediaRetryLock: semaphore.NewWeighted(wa.Config.HistorySync.MediaRequests.MaxAsyncHandle), pushNamesSynced: exsync.NewEvent(), + createDedup: exsync.NewSet[types.MessageID](), appStateFullSyncAttempted: make(map[appstate.WAPatchName]time.Time), - - disableNewsletter: store.BaseClientPayload.GetUserAgent().GetPlatform() == waWa6.ClientPayload_UserAgent_MACOS, } login.Client = w @@ -71,14 +70,13 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2. if err != nil { return err } - w.LID = w.Device.GetLID() if w.Device != nil { log := w.UserLogin.Log.With().Str("component", "whatsmeow").Logger() w.Client = whatsmeow.NewClient(w.Device, waLog.Zerolog(log)) w.Client.AddEventHandlerWithSuccessStatus(w.handleWAEvent) w.Client.SynchronousAck = true - w.Client.EnableDecryptedEventBuffer = wa.Bridge.Config.PortalEventBuffer == 0 + w.Client.EnableDecryptedEventBuffer = bridgev2.PortalEventBuffer == 0 w.Client.ManualHistorySyncDownload = true w.Client.SendReportingTokens = true w.Client.AutomaticMessageRerequestFromPhone = true @@ -106,7 +104,6 @@ type WhatsAppClient struct { Client *whatsmeow.Client Device *store.Device JID types.JID - LID types.JID MC mClient historySyncWakeup chan struct{} @@ -116,14 +113,12 @@ type WhatsAppClient struct { nextResync time.Time directMediaRetries map[networkid.MessageID]*directMediaRetry directMediaLock sync.Mutex - avatarLock exsync.KeyedMutex[types.JID] mediaRetryLock *semaphore.Weighted offlineSyncWaiter atomic.Pointer[chan error] isNewLogin bool pushNamesSynced *exsync.Event lastPresence types.Presence - - disableNewsletter bool + createDedup *exsync.Set[types.MessageID] appStateRecoveryLock sync.Mutex appStateFullSyncAttempted map[appstate.WAPatchName]time.Time @@ -190,19 +185,7 @@ func (wa *WhatsAppClient) RegisterPushNotifications(ctx context.Context, pushTyp } func (wa *WhatsAppClient) IsThisUser(_ context.Context, userID networkid.UserID) bool { - return userID == waid.MakeUserID(wa.JID) || userID == waid.MakeUserID(wa.GetLID()) -} - -func (wa *WhatsAppClient) IsOwnJID(jid types.JID) bool { - return (jid.Server == types.DefaultUserServer && jid.User == wa.JID.User) || - (jid.Server == types.HiddenUserServer && jid.User == wa.GetLID().User) -} - -func (wa *WhatsAppClient) GetLID() types.JID { - if wa.LID.IsEmpty() && !wa.JID.IsEmpty() { - wa.LID = wa.GetStore().GetLID() - } - return wa.LID + return userID == waid.MakeUserID(wa.JID) } func (wa *WhatsAppClient) Connect(ctx context.Context) { @@ -394,10 +377,6 @@ func (wa *WhatsAppClient) LogoutRemote(ctx context.Context) { } wa.Disconnect() wa.Client = nil - err := wa.Main.DB.Conversation.DeleteAll(ctx, wa.UserLogin.ID) - if err != nil { - zerolog.Ctx(ctx).Err(err).Msg("Failed to delete history sync data on logout") - } } func (wa *WhatsAppClient) IsLoggedIn() bool { diff --git a/pkg/connector/config.go b/pkg/connector/config.go index 24874a2..e51c80a 100644 --- a/pkg/connector/config.go +++ b/pkg/connector/config.go @@ -52,8 +52,6 @@ type Config struct { DirectMediaAutoRequest bool `yaml:"direct_media_auto_request"` InitialAutoReconnect bool `yaml:"initial_auto_reconnect"` UseWhatsAppRetryStore bool `yaml:"use_whatsapp_retry_store"` - MaxMemberSync int `yaml:"max_member_sync"` - LazyAvatars bool `yaml:"lazy_avatars"` AnimatedSticker msgconv.AnimatedStickerConfig `yaml:"animated_sticker"` @@ -131,8 +129,6 @@ func upgradeConfig(helper up.Helper) { helper.Copy(up.Bool, "direct_media_auto_request") helper.Copy(up.Bool, "initial_auto_reconnect") helper.Copy(up.Bool, "use_whatsapp_retry_store") - helper.Copy(up.Int, "max_member_sync") - helper.Copy(up.Bool, "lazy_avatars") helper.Copy(up.Str, "animated_sticker", "target") helper.Copy(up.Int, "animated_sticker", "args", "width") @@ -209,7 +205,6 @@ func (wa *WhatsAppConnector) GetConfig() (string, any, up.Upgrader) { {"proxy"}, {"displayname_template"}, {"call_start_notices"}, - {"animated_sticker"}, {"history_sync"}, }, Base: ExampleConfig, diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index d7261d3..91b1e11 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -28,24 +28,27 @@ import ( "time" "github.com/lib/pq" + "github.com/rs/zerolog" "go.mau.fi/util/dbutil" - "go.mau.fi/util/exsync" "go.mau.fi/util/random" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/proto/waCompanionReg" "go.mau.fi/whatsmeow/store" "go.mau.fi/whatsmeow/store/sqlstore" whatsmeowUpgrades "go.mau.fi/whatsmeow/store/sqlstore/upgrades" + "go.mau.fi/whatsmeow/types" waLog "go.mau.fi/whatsmeow/util/log" "google.golang.org/protobuf/proto" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/commands" + "maunium.net/go/mautrix/bridgev2/database" "maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" "go.mau.fi/mautrix-whatsapp/pkg/connector/wadb" "go.mau.fi/mautrix-whatsapp/pkg/msgconv" + "go.mau.fi/mautrix-whatsapp/pkg/waid" ) type WhatsAppConnector struct { @@ -61,8 +64,6 @@ type WhatsAppConnector struct { mediaEditCache MediaEditCache mediaEditCacheLock sync.RWMutex stopMediaEditCacheLoop atomic.Pointer[context.CancelFunc] - - unmigratedDMs *exsync.Set[networkid.PortalKey] } func init() { @@ -113,7 +114,6 @@ func (wa *WhatsAppConnector) Init(bridge *bridgev2.Bridge) { cmdAccept, cmdSync, cmdInviteLink, cmdResolveLink, cmdJoin, ) wa.mediaEditCache = make(MediaEditCache) - wa.unmigratedDMs = exsync.NewSet[networkid.PortalKey]() whatsmeowDBLog := bridge.Log.With().Str("db_section", "whatsmeow").Logger() wa.DeviceStore = sqlstore.NewWithWrappedDB( @@ -142,9 +142,6 @@ func (wa *WhatsAppConnector) Init(bridge *bridgev2.Bridge) { } func (wa *WhatsAppConnector) Start(ctx context.Context) error { - if !wa.MsgConv.DirectMedia && wa.Config.LazyAvatars { - return fmt.Errorf("lazy_avatars set without enabling global direct_media") - } err := wa.DeviceStore.Upgrade(ctx) if err != nil { return bridgev2.DBUpgradeError{Err: err, Section: "whatsmeow"} @@ -160,19 +157,80 @@ func (wa *WhatsAppConnector) Start(ctx context.Context) error { return bridgev2.DBUpgradeError{Err: err, Section: "whatsapp"} } - err = wa.migrateToLIDDMs(ctx) - if err != nil { - return fmt.Errorf("failed to migrate to LID DMs: %w", err) + if !wa.Bridge.Background && wa.Bridge.DB.KV.Get(ctx, "whatsapp_lid_dms_deleted") == "false" { + wa.deleteLIDDMsMigration(ctx) } - go func() { - err = wa.syncMismatchingGhosts(wa.Bridge.BackgroundCtx) - if err != nil { - wa.Bridge.Log.Err(err).Msg("Failed to sync mismatching ghosts") - } - }() + return nil } +func (wa *WhatsAppConnector) deleteLIDDMsMigration(ctx context.Context) { + log := zerolog.Ctx(ctx).With().Str("action", "delete lid dms").Logger() + portals, err := wa.Bridge.GetAllPortalsWithMXID(ctx) + if err != nil { + log.Err(err).Msg("Failed to get portals for LID DM deletion") + return + } + defer wa.Bridge.DB.KV.Set(ctx, "whatsapp_lid_dms_deleted", "true") + if len(portals) == 0 { + log.Debug().Msg("No portals found") + return + } + portalsByKey := make(map[networkid.PortalKey]*bridgev2.Portal, len(portals)) + for _, p := range portals { + if p.Receiver == "" || p.RoomType != database.RoomTypeDM { + continue + } + portalsByKey[p.PortalKey] = p + } + _, err = wa.DB.Exec(ctx, "DELETE FROM whatsapp_history_sync_conversation WHERE chat_jid LIKE '%@lid'") + if err != nil { + log.Err(err).Msg("Failed to remove LID conversations from history sync") + } + for key, portal := range portalsByKey { + parsedID, err := waid.ParsePortalID(key.ID) + if err != nil { + log.Warn().Err(err).Str("portal_id", string(key.ID)).Msg("Failed to parse portal ID") + continue + } else if parsedID.Server != types.HiddenUserServer { + continue + } + var pnStr string + err = wa.DB.QueryRow(ctx, "SELECT pn FROM whatsmeow_lid_map WHERE lid=$1", parsedID.User).Scan(&pnStr) + if err != nil { + log.Warn().Err(err).Str("portal_id", string(key.ID)).Msg("Failed to get PN for LID portal") + continue + } + key.ID = waid.MakePortalID(types.JID{User: pnStr, Server: types.DefaultUserServer}) + _, pnPortalExists := portalsByKey[key] + if !pnPortalExists { + log.Warn().Str("portal_id", string(key.ID)).Msg("PN portal does not exist, not deleting LID DM") + continue + } + err = portal.Delete(ctx) + if err != nil { + log.Err(err). + Object("portal_key", portal.PortalKey). + Stringer("portal_mxid", portal.MXID). + Msg("Failed to delete LID DM portal from database") + continue + } + err = wa.Bridge.Bot.DeleteRoom(ctx, portal.MXID, false) + if err != nil { + log.Err(err). + Object("portal_key", portal.PortalKey). + Stringer("portal_mxid", portal.MXID). + Msg("Failed to delete LID DM portal from Matrix") + continue + } + log.Debug(). + Object("portal_key", portal.PortalKey). + Stringer("portal_mxid", portal.MXID). + Msg("Deleted LID DM portal") + } + log.Info().Msg("Finished deleting LID DM portals") +} + func (wa *WhatsAppConnector) Stop() { if stop := wa.stopMediaEditCacheLoop.Swap(nil); stop != nil { (*stop)() diff --git a/pkg/connector/directmedia.go b/pkg/connector/directmedia.go index 8ed94ce..89a54fc 100644 --- a/pkg/connector/directmedia.go +++ b/pkg/connector/directmedia.go @@ -30,7 +30,6 @@ import ( "github.com/rs/zerolog" "go.mau.fi/util/exsync" - "go.mau.fi/util/jsontime" "go.mau.fi/util/ptr" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/proto/waMmsRetry" @@ -53,7 +52,7 @@ func (wa *WhatsAppConnector) SetUseDirectMedia() { } var ErrReloadNeeded = mautrix.RespError{ - ErrCode: "COM.BEEPER.MEDIA_RELOAD_NEEDED", + ErrCode: "FI.MAU.WHATSAPP_RELOAD_NEEDED", Err: "Media is no longer available on WhatsApp servers and must be re-requested from your phone", StatusCode: http.StatusNotFound, } @@ -85,15 +84,13 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars if waClient.Client == nil { return nil, fmt.Errorf("no WhatsApp client found on login %s", parsedID.UserLogin) } - waClient.avatarLock.Lock(parsedID.Avatar.TargetJID) - defer waClient.avatarLock.Unlock(parsedID.Avatar.TargetJID) cachedInfo, err := wa.DB.AvatarCache.Get(ctx, parsedID.Avatar.TargetJID, parsedID.Avatar.AvatarID) if err != nil { return nil, fmt.Errorf("failed to get avatar cache entry: %w", err) } - if cachedInfo.IsGone() { + if cachedInfo != nil && cachedInfo.Gone { return nil, mautrix.MNotFound.WithMessage("Avatar is no longer available (cached response)") - } else if cachedInfo.Expired() { + } else if cachedInfo == nil || cachedInfo.Expiry.Time.Before(time.Now().Add(5*time.Minute)) { zerolog.Ctx(ctx).Debug(). Str("avatar_id", parsedID.Avatar.AvatarID). Msg("Refreshing avatar URL from WhatsApp servers") @@ -102,7 +99,7 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars }) if errors.Is(err, whatsmeow.ErrProfilePictureNotSet) || errors.Is(err, whatsmeow.ErrProfilePictureUnauthorized) || - (err == nil && (avatar == nil || (avatar.ID != parsedID.Avatar.AvatarID && !parsedID.Avatar.IsRandom()))) { + (err == nil && (avatar == nil || avatar.ID != parsedID.Avatar.AvatarID)) { zerolog.Ctx(ctx).Debug(). Err(err). Stringer("target_jid", parsedID.Avatar.TargetJID). @@ -110,14 +107,9 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars Str("wanted_avatar_id", parsedID.Avatar.AvatarID). Str("got_avatar_id", ptr.Val(avatar).ID). Msg("Avatar is no longer available") - var goneExpiry jsontime.Unix - if parsedID.Avatar.IsRandom() { - goneExpiry = jsontime.U(time.Now().Add(7 * 24 * time.Hour)) - } err = wa.DB.AvatarCache.Put(ctx, &wadb.AvatarCacheEntry{ EntityJID: parsedID.Avatar.TargetJID, AvatarID: parsedID.Avatar.AvatarID, - Expiry: goneExpiry, Gone: true, }) if err != nil { @@ -136,15 +128,6 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars Str("avatar_id", avatar.ID). Msg("Failed to update avatar cache entry") } - if cachedInfo.AvatarID != parsedID.Avatar.AvatarID { - cachedInfo.AvatarID = parsedID.Avatar.AvatarID - err = wa.DB.AvatarCache.Put(ctx, cachedInfo) - if err != nil { - zerolog.Ctx(ctx).Warn().Err(err). - Str("avatar_id", parsedID.Avatar.AvatarID). - Msg("Failed to update avatar cache entry") - } - } } return &mediaproxy.GetMediaResponseFile{ Callback: func(w *os.File) (*mediaproxy.FileMeta, error) { @@ -229,7 +212,7 @@ func (wa *WhatsAppConnector) makeDirectMediaResponse( log := zerolog.Ctx(ctx) err := waClient.Client.DownloadToFile(ctx, dm, f) if keys != nil && (errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith403) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith410) || errors.Is(err, whatsmeow.ErrNoURLPresent)) { - val := params["com.beeper.interactive_download_request"] + val := params["fi.mau.whatsapp.reload_media"] if val == "false" || (!wa.Config.DirectMediaAutoRequest && val != "true") { return nil, ErrReloadNeeded } @@ -272,11 +255,10 @@ func (wa *WhatsAppConnector) makeDirectMediaResponse( type directMediaRetry struct { sync.Mutex - resultURL string - wait *exsync.Event - requested bool - resultType waMmsRetry.MediaRetryNotification_ResultType - decryptFail bool + resultURL string + wait *exsync.Event + requested bool + resultType waMmsRetry.MediaRetryNotification_ResultType } func (wa *WhatsAppClient) getDirectMediaRetryState(msgID networkid.MessageID, create bool) *directMediaRetry { @@ -303,9 +285,6 @@ func (wa *WhatsAppClient) requestAndWaitDirectMedia(ctx context.Context, rawMsgI keys.DirectPath = state.resultURL return nil } - if state.decryptFail { - return mautrix.MNotFound.WithMessage("Unable to retrieve media: failed to decrypt the media retry notification from your phone.") - } switch state.resultType { case waMmsRetry.MediaRetryNotification_NOT_FOUND: return mautrix.MNotFound.WithMessage("This media was not found on your phone.") @@ -359,9 +338,6 @@ func (wa *WhatsAppClient) receiveDirectMediaRetry(ctx context.Context, msg *data retryData, err := whatsmeow.DecryptMediaRetryNotification(retry, keys.Key) if err != nil { log.Warn().Err(err).Msg("Failed to decrypt media retry notification") - if state != nil { - state.decryptFail = true - } return } if state != nil { diff --git a/pkg/connector/events.go b/pkg/connector/events.go index 1f4e118..cb51e64 100644 --- a/pkg/connector/events.go +++ b/pkg/connector/events.go @@ -39,9 +39,22 @@ import ( "go.mau.fi/mautrix-whatsapp/pkg/waid" ) +func (wa *WhatsAppClient) getPortalKeyByMessageSource(ms types.MessageSource) networkid.PortalKey { + jid := ms.Chat + if ms.IsIncomingBroadcast() { + if ms.IsFromMe { + jid = ms.BroadcastListOwner.ToNonAD() + } else { + jid = ms.Sender.ToNonAD() + } + } + return wa.makeWAPortalKey(jid) +} + type MessageInfoWrapper struct { - Info types.MessageInfo - wa *WhatsAppClient + OrigSource types.MessageSource + Info types.MessageInfo + wa *WhatsAppClient } func (evt *MessageInfoWrapper) ShouldCreatePortal() bool { @@ -49,26 +62,7 @@ func (evt *MessageInfoWrapper) ShouldCreatePortal() bool { } func (evt *MessageInfoWrapper) GetPortalKey() networkid.PortalKey { - ms := evt.Info.MessageSource - jid := ms.Chat - if ms.IsIncomingBroadcast() { - if ms.IsFromMe { - // TODO can this still be a phone number? - jid = ms.BroadcastListOwner.ToNonAD() - } else { - jid = ms.Sender.ToNonAD() - if jid.Server == types.DefaultUserServer && !ms.SenderAlt.IsEmpty() { - jid = ms.SenderAlt.ToNonAD() - } - } - } else if jid.Server == types.DefaultUserServer { - if !ms.IsFromMe && ms.Chat.ToNonAD() == ms.Sender.ToNonAD() && !ms.SenderAlt.IsEmpty() { - jid = ms.SenderAlt.ToNonAD() - } else if !ms.RecipientAlt.IsEmpty() { - jid = ms.RecipientAlt.ToNonAD() - } - } - return evt.wa.makeWAPortalKey(jid) + return evt.wa.getPortalKeyByMessageSource(evt.Info.MessageSource) } func (evt *MessageInfoWrapper) AddLogContext(c zerolog.Context) zerolog.Context { @@ -79,19 +73,12 @@ func (evt *MessageInfoWrapper) GetTimestamp() time.Time { return evt.Info.Timestamp } -func pickLID(main, alt types.JID) types.JID { - if main.Server == types.DefaultUserServer && alt.Server == types.HiddenUserServer { - return alt - } - return main -} - func (evt *MessageInfoWrapper) GetSender() bridgev2.EventSender { - return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, pickLID(evt.Info.Sender, evt.Info.SenderAlt)) + return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, evt.Info.Sender) } func (evt *MessageInfoWrapper) GetID() networkid.MessageID { - return waid.MakeMessageIDWithAltSender(evt.Info.Chat, evt.Info.Sender, evt.Info.SenderAlt, evt.Info.ID) + return waid.MakeMessageID(evt.Info.Chat, evt.Info.Sender, evt.Info.ID) } func (evt *MessageInfoWrapper) GetTransactionID() networkid.TransactionID { @@ -148,6 +135,14 @@ func (evt *WAMessageEvent) PreHandle(ctx context.Context, portal *bridgev2.Porta return } meta := portal.Metadata.(*waid.PortalMetadata) + if meta.AddressingMode == types.AddressingModeLID && evt.Info.Sender.Server == types.DefaultUserServer { + evt.Info.Sender, evt.Info.SenderAlt = evt.Info.SenderAlt, evt.Info.Sender + zerolog.Ctx(ctx).Debug(). + Stringer("lid", evt.Info.Sender). + Stringer("pn", evt.Info.SenderAlt). + Str("message_id", evt.Info.ID). + Msg("Forced phone number sender to LID in group message") + } if meta.AddressingMode == types.AddressingModeLID || meta.LIDMigrationAttempted { return } @@ -166,6 +161,13 @@ func (evt *WAMessageEvent) PreHandle(ctx context.Context, portal *bridgev2.Porta log.Info().Msg("Resyncing group members as it appears to have switched to LID addressing mode") portal.UpdateInfo(ctx, evt.wa.wrapGroupInfo(ctx, info), evt.wa.UserLogin, nil, time.Time{}) log.Debug().Msg("Finished resyncing after LID change") + if evt.Info.Sender.Server == types.DefaultUserServer && evt.Info.SenderAlt.Server == types.HiddenUserServer { + evt.Info.Sender, evt.Info.SenderAlt = evt.Info.SenderAlt, evt.Info.Sender + log.Debug(). + Stringer("new_sender", evt.Info.Sender). + Stringer("new_sender_alt", evt.Info.SenderAlt). + Msg("Overriding sender to LID after resyncing group members") + } } func (evt *WAMessageEvent) PostHandle(ctx context.Context, portal *bridgev2.Portal) { @@ -199,7 +201,7 @@ func (evt *WAMessageEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Por ctx = context.WithValue(ctx, msgconv.ContextKeyEditTargetID, evt.Message.GetProtocolMessage().GetKey().GetID()) cm := evt.wa.Main.MsgConv.ToMatrix( - ctx, portal, evt.wa.Client, intent, editedMsg, evt.MsgEvent.RawMessage, &evt.Info, evt.isViewOnce(), false, previouslyConvertedPart, + ctx, portal, evt.wa.Client, intent, editedMsg, evt.MsgEvent.RawMessage, &evt.Info, &evt.OrigSource, evt.isViewOnce(), false, previouslyConvertedPart, ) if evt.isUndecryptableUpsertSubEvent && isFailedMedia(cm) { evt.postHandle = func() { @@ -284,7 +286,7 @@ func (evt *WAMessageEvent) HandleExisting(ctx context.Context, portal *bridgev2. func (evt *WAMessageEvent) ConvertMessage(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI) (*bridgev2.ConvertedMessage, error) { evt.wa.EnqueuePortalResync(portal, false) converted := evt.wa.Main.MsgConv.ToMatrix( - ctx, portal, evt.wa.Client, intent, evt.Message, evt.MsgEvent.RawMessage, &evt.Info, evt.isViewOnce(), false, nil, + ctx, portal, evt.wa.Client, intent, evt.Message, evt.MsgEvent.RawMessage, &evt.Info, &evt.OrigSource, evt.isViewOnce(), false, nil, ) if isFailedMedia(converted) { evt.postHandle = func() { @@ -390,9 +392,7 @@ func (evt *WAUndecryptableMessage) GetStreamOrder() int64 { type WAMediaRetry struct { *events.MediaRetry - wa *WhatsAppClient - senderLID types.JID - chatLID types.JID + wa *WhatsAppClient } func (evt *WAMediaRetry) GetType() bridgev2.RemoteEventType { @@ -400,7 +400,7 @@ func (evt *WAMediaRetry) GetType() bridgev2.RemoteEventType { } func (evt *WAMediaRetry) GetPortalKey() networkid.PortalKey { - return evt.wa.makeWAPortalKey(pickLID(evt.ChatID, evt.chatLID)) + return evt.wa.makeWAPortalKey(evt.ChatID) } func (evt *WAMediaRetry) AddLogContext(c zerolog.Context) zerolog.Context { @@ -414,23 +414,16 @@ func (evt *WAMediaRetry) AddLogContext(c zerolog.Context) zerolog.Context { func (evt *WAMediaRetry) getRealSender() types.JID { sender := evt.SenderID - if sender.IsEmpty() { - if evt.FromMe { - if evt.ChatID.Server == types.HiddenUserServer { - sender = evt.wa.GetLID().ToNonAD() - } else { - sender = evt.wa.JID.ToNonAD() - } - } else if evt.ChatID.Server == types.DefaultUserServer || evt.ChatID.Server == types.HiddenUserServer || evt.ChatID.Server == types.BotServer { - sender = evt.ChatID.ToNonAD() - } + if evt.FromMe { + sender = evt.wa.JID.ToNonAD() + } else if sender.IsEmpty() && (evt.ChatID.Server == types.DefaultUserServer || evt.ChatID.Server == types.BotServer) { + sender = evt.ChatID.ToNonAD() } return sender } func (evt *WAMediaRetry) GetSender() bridgev2.EventSender { - realSender := pickLID(evt.getRealSender(), evt.senderLID) - return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, realSender) + return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, evt.getRealSender()) } func (evt *WAMediaRetry) GetTargetMessage() networkid.MessageID { diff --git a/pkg/connector/example-config.yaml b/pkg/connector/example-config.yaml index 905e6d6..564f25e 100644 --- a/pkg/connector/example-config.yaml +++ b/pkg/connector/example-config.yaml @@ -69,12 +69,6 @@ initial_auto_reconnect: true # the bridge? By default, the bridge only stores messages in memory, and therefore can't accept # retry receipts if the bridge is restarted after the message is sent. use_whatsapp_retry_store: false -# Maximum size of groups to sync members eagerly in. Defaults to unlimited. -# If set, groups with more than this number of members will not have non-admin participants synced until they talk. -max_member_sync: -1 -# Lazily load avatars using direct media? -# Global direct media must be enabled for this. -lazy_avatars: false # Settings for converting animated stickers. animated_sticker: diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go index c55f523..9aea18b 100644 --- a/pkg/connector/handlematrix.go +++ b/pkg/connector/handlematrix.go @@ -103,14 +103,11 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg if err != nil { return nil, err } - if chatJID.Server == types.DefaultUserServer { - zerolog.Ctx(ctx).Warn().Stringer("portal_jid", chatJID).Msg("Matrix message received in phone number portal") - } if chatJID == types.StatusBroadcastJID && wa.Main.Config.DisableStatusBroadcastSend { return nil, ErrBroadcastSendDisabled } wrappedMsgID := waid.MakeMessageID(chatJID, wa.JID, req.ID) - wrappedMsgID2 := waid.MakeMessageID(chatJID, wa.GetLID(), req.ID) + wrappedMsgID2 := waid.MakeMessageID(chatJID, wa.GetStore().GetLID(), req.ID) msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID)) msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID2)) zerolog.Ctx(ctx).Trace().Any("payload", waMsg).Msg("Outgoing message payload") @@ -119,7 +116,7 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg return nil, err } var pickedMessageID networkid.MessageID - if resp.Sender == wa.GetLID() { + if resp.Sender == wa.GetStore().GetLID() && chatJID.Server != types.DefaultUserServer { pickedMessageID = wrappedMsgID2 msg.RemovePending(networkid.TransactionID(wrappedMsgID)) } else { @@ -140,17 +137,18 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg }, nil } -func (wa *WhatsAppClient) PreHandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (bridgev2.MatrixReactionPreResponse, error) { +func (wa *WhatsAppClient) PreHandleMatrixReaction(_ context.Context, msg *bridgev2.MatrixReaction) (bridgev2.MatrixReactionPreResponse, error) { portalJID, err := waid.ParsePortalID(msg.Portal.ID) if err != nil { return bridgev2.MatrixReactionPreResponse{}, fmt.Errorf("failed to parse portal ID: %w", err) } else if portalJID == types.StatusBroadcastJID { return bridgev2.MatrixReactionPreResponse{}, ErrBroadcastReactionUnsupported } - sender := wa.GetLID() - if portalJID.Server == types.DefaultUserServer { - zerolog.Ctx(ctx).Warn().Stringer("portal_jid", portalJID).Msg("Matrix reaction received in phone number portal") - sender = wa.JID + sender := wa.JID + if portalJID.Server == types.HiddenUserServer || + msg.Portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup || + msg.Portal.Metadata.(*waid.PortalMetadata).AddressingMode == types.AddressingModeLID { + sender = wa.GetStore().GetLID() } return bridgev2.MatrixReactionPreResponse{ SenderID: waid.MakeUserID(sender), @@ -322,7 +320,7 @@ func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt * if err != nil { continue } - if wa.IsOwnJID(parsed.Sender) { + if parsed.Sender.User == wa.GetStore().GetLID().User || parsed.Sender.User == wa.JID.User { continue } var key types.JID @@ -638,7 +636,7 @@ func (wa *WhatsAppClient) getLastMessageInfo(ctx context.Context, chatJID types. lastTS = msgs[0].Timestamp parsed, _ := waid.ParseMessageID(msgs[0].ID) if parsed != nil { - fromMe := wa.IsOwnJID(parsed.Sender) + fromMe := parsed.Sender.ToNonAD() == wa.JID.ToNonAD() || parsed.Sender.ToNonAD() == wa.GetStore().GetLID().ToNonAD() var participant *string if chatJID.Server == types.GroupServer { participant = ptr.Ptr(parsed.Sender.String()) diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index 5381311..f12ebad 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -24,11 +24,9 @@ import ( "time" "github.com/rs/zerolog" - "go.mau.fi/util/exslices" "go.mau.fi/util/ptr" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/appstate" - "go.mau.fi/whatsmeow/proto/waCommon" "go.mau.fi/whatsmeow/proto/waE2E" "go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types/events" @@ -115,7 +113,8 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { case *events.HistorySync: wa.UserLogin.Log.Warn().Msg("Unexpected history sync event received") case *events.MediaRetry: - success = wa.handleWAMediaRetry(ctx, evt) + wa.phoneSeen(evt.Timestamp) + success = wa.UserLogin.QueueRemoteEvent(&WAMediaRetry{MediaRetry: evt, wa: wa}).Success case *events.GroupInfo: success = wa.handleWAGroupInfoChange(ctx, evt) @@ -159,7 +158,7 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { if err != nil { log.Err(err).Msg("Failed to update push name in store") } - _, _, err = wa.GetStore().Contacts.PutPushName(ctx, wa.GetLID().ToNonAD(), evt.Action.GetName()) + _, _, err = wa.GetStore().Contacts.PutPushName(ctx, wa.GetStore().GetLID().ToNonAD(), evt.Action.GetName()) if err != nil { log.Err(err).Msg("Failed to update push name in store") } @@ -204,7 +203,7 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected}) wa.notifyOfflineSyncWaiter(nil) case *events.LoggedOut: - wa.handleWALogout(ctx, evt.Reason, evt.OnConnect) + wa.handleWALogout(evt.Reason, evt.OnConnect) wa.notifyOfflineSyncWaiter(fmt.Errorf("logged out: %s", evt.Reason)) case *events.Disconnected: // Don't send the normal transient disconnect state if we're already in a different transient disconnect state. @@ -260,38 +259,72 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { return } -func (wa *WhatsAppClient) ensureAltJIDs(ctx context.Context, info *types.MessageSource, checkPhones bool) bool { - var err error - if info.Sender.Server == types.DefaultUserServer && info.SenderAlt.IsEmpty() { - info.SenderAlt, err = wa.GetStore().LIDs.GetLIDForPN(ctx, info.Sender) +func (wa *WhatsAppClient) rerouteWAMessage(ctx context.Context, evtType string, info *types.MessageSource, msgID any) { + if (info.Chat.Server == types.HiddenUserServer || info.Chat.Server == types.BroadcastServer) && + info.Sender.Server == types.HiddenUserServer && info.SenderAlt.IsEmpty() { + info.SenderAlt, _ = wa.GetStore().LIDs.GetPNForLID(ctx, info.Sender) + } + if info.Chat.Server == types.HiddenUserServer && info.IsFromMe && info.RecipientAlt.IsEmpty() { + info.RecipientAlt, _ = wa.GetStore().LIDs.GetPNForLID(ctx, info.Chat) + } + if info.Chat.Server == types.HiddenUserServer && info.Sender.ToNonAD() == info.Chat && info.SenderAlt.Server == types.DefaultUserServer { + wa.UserLogin.Log.Debug(). + Stringer("lid", info.Sender). + Stringer("pn", info.SenderAlt). + Any("message_id", msgID). + Str("evt_type", evtType). + Msg("Forced LID DM sender to phone number in incoming message") + info.Sender, info.SenderAlt = info.SenderAlt, info.Sender + info.Chat = info.Sender.ToNonAD() + } else if info.Chat.Server == types.HiddenUserServer && info.IsFromMe && info.RecipientAlt.Server == types.DefaultUserServer { + wa.UserLogin.Log.Debug(). + Stringer("lid", info.Chat). + Stringer("pn", info.RecipientAlt). + Any("message_id", msgID). + Str("evt_type", evtType). + Msg("Forced LID DM sender to phone number in own message sent from another device") + info.Chat = info.RecipientAlt.ToNonAD() + if info.Sender.Server == types.HiddenUserServer { + info.Sender, info.SenderAlt = info.SenderAlt, info.Sender + if info.Sender.IsEmpty() { + info.Sender = wa.GetStore().GetJID() + info.Sender.Device = info.SenderAlt.Device + } + } + } else if info.Chat.Server == types.BroadcastServer && info.Sender.Server == types.HiddenUserServer && info.SenderAlt.Server == types.DefaultUserServer { + wa.UserLogin.Log.Debug(). + Stringer("lid", info.Sender). + Stringer("pn", info.SenderAlt). + Stringer("chat", info.Chat). + Any("message_id", msgID). + Str("evt_type", evtType). + Msg("Forced LID broadcast list sender to phone number in incoming message") + info.Sender, info.SenderAlt = info.SenderAlt, info.Sender + } else if info.Sender.Server == types.BotServer && info.Chat.Server == types.HiddenUserServer { + chatPN, err := wa.GetStore().LIDs.GetPNForLID(ctx, info.Chat) if err != nil { - zerolog.Ctx(ctx).Err(err).Stringer("sender", info.Sender).Msg("Failed to get LID for sender") - return false + wa.UserLogin.Log.Err(err). + Any("message_id", msgID). + Stringer("lid", info.Chat). + Str("evt_type", evtType). + Msg("Failed to get phone number of DM for incoming bot message") + } else if !chatPN.IsEmpty() { + wa.UserLogin.Log.Debug(). + Stringer("lid", info.Chat). + Stringer("pn", chatPN). + Any("message_id", msgID). + Str("evt_type", evtType). + Msg("Forced LID chat to phone number in bot message") + info.Chat = chatPN } } - if info.Chat.Server == types.DefaultUserServer && info.IsFromMe && info.RecipientAlt.IsEmpty() { - info.RecipientAlt, err = wa.GetStore().LIDs.GetLIDForPN(ctx, info.Chat) - if err != nil { - zerolog.Ctx(ctx).Err(err).Stringer("chat", info.Chat).Msg("Failed to get LID for chat") - return false - } - } - if checkPhones { - return wa.checkAllPhonesInMessage(ctx, info) - } - return true } func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Message) (success bool) { success = true - if (evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast) || - (evt.Info.Chat.Server == types.NewsletterServer && wa.disableNewsletter) || - (evt.Info.Chat.Server == types.BroadcastServer && evt.Info.IsFromMe) { + if evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast { return } - if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, true) { - return false - } parsedMessageType := getMessageType(evt.Message) if encReact := evt.Message.GetEncReactionMessage(); encReact != nil { decrypted, err := wa.Client.DecryptReaction(ctx, evt) @@ -325,6 +358,8 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa evt.UnwrapRaw() parsedMessageType = getMessageType(evt.Message) } + origSource := evt.Info.MessageSource + wa.rerouteWAMessage(ctx, "message", &evt.Info.MessageSource, evt.Info.ID) wa.UserLogin.Log.Trace(). Any("info", evt.Info). Any("payload", evt.Message). @@ -334,15 +369,7 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa wa.Main.Bridge.Config.Backfill.Enabled { wa.saveWAHistorySyncNotification(ctx, evt.Message.ProtocolMessage.HistorySyncNotification) } - if parsedMessageType == "ignore" { - return - } else if strings.HasPrefix(parsedMessageType, "unknown_protocol_") { - wa.UserLogin.Log.Debug(). - Str("message_id", evt.Info.ID). - Stringer("chat_jid", evt.Info.Chat). - Stringer("sender_jid", evt.Info.Sender). - Stringer("protocol_message_type", evt.Message.GetProtocolMessage().GetType()). - Msg("Ignoring unknown protocol message") + if parsedMessageType == "ignore" || strings.HasPrefix(parsedMessageType, "unknown_protocol_") { return } @@ -350,8 +377,18 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa messageAssoc := evt.Message.GetMessageContextInfo().GetMessageAssociation() if assocType := messageAssoc.GetAssociationType(); assocType == waE2E.MessageAssociation_HD_IMAGE_DUAL_UPLOAD || assocType == waE2E.MessageAssociation_HD_VIDEO_DUAL_UPLOAD { parentKey := messageAssoc.GetParentMessageKey() - protocolMsg, shouldHideEdit := makeHDMediaReplacementEdit(evt.Message, parentKey) - dontRenderEdited = shouldHideEdit + protocolMsg := evt.Message.GetProtocolMessage() + if protocolMsg.GetType() != waE2E.ProtocolMessage_MESSAGE_EDIT || protocolMsg.GetKey() == nil { + protocolMsg = &waE2E.ProtocolMessage{ + Type: waE2E.ProtocolMessage_MESSAGE_EDIT.Enum(), + Key: parentKey, + EditedMessage: evt.Message.GetAssociatedChildMessage().GetMessage(), + } + dontRenderEdited = true + } else if child := protocolMsg.GetEditedMessage().GetAssociatedChildMessage().GetMessage(); child != nil { + protocolMsg.EditedMessage = child + protocolMsg.Key = parentKey + } wa.UserLogin.Log.Debug(). Str("message_id", evt.Info.ID). Str("parent_id", parentKey.GetID()). @@ -373,8 +410,9 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa res := wa.UserLogin.QueueRemoteEvent(&WAMessageEvent{ MessageInfoWrapper: &MessageInfoWrapper{ - Info: evt.Info, - wa: wa, + OrigSource: origSource, + Info: evt.Info, + wa: wa, }, Message: evt.Message, MsgEvent: evt, @@ -385,30 +423,8 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa return res.Success } -func makeHDMediaReplacementEdit(message *waE2E.Message, parentKey *waCommon.MessageKey) (*waE2E.ProtocolMessage, bool) { - protocolMsg := message.GetProtocolMessage() - associatedMessage := message.GetAssociatedChildMessage().GetMessage() - if protocolMsg.GetType() != waE2E.ProtocolMessage_MESSAGE_EDIT || protocolMsg.GetKey() == nil { - protocolMsg = associatedMessage.GetProtocolMessage() - } - if protocolMsg.GetType() == waE2E.ProtocolMessage_MESSAGE_EDIT && protocolMsg.GetKey() != nil { - if child := protocolMsg.GetEditedMessage().GetAssociatedChildMessage().GetMessage(); child != nil { - protocolMsg.EditedMessage = child - } - protocolMsg.Key = parentKey - return protocolMsg, false - } - return &waE2E.ProtocolMessage{ - Type: waE2E.ProtocolMessage_MESSAGE_EDIT.Enum(), - Key: parentKey, - EditedMessage: associatedMessage, - }, true -} - func (wa *WhatsAppClient) handleWAUndecryptableMessage(ctx context.Context, evt *events.UndecryptableMessage) bool { - if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, true) { - return false - } + wa.rerouteWAMessage(ctx, "undecryptable message", &evt.Info.MessageSource, evt.Info.ID) wa.UserLogin.Log.Debug(). Any("info", evt.Info). Bool("unavailable", evt.IsUnavailable). @@ -431,44 +447,12 @@ func (wa *WhatsAppClient) handleWAUndecryptableMessage(ctx context.Context, evt return res.Success } -func (wa *WhatsAppClient) handleWAMediaRetry(ctx context.Context, evt *events.MediaRetry) bool { - wa.phoneSeen(evt.Timestamp) - var senderLID, chatLID types.JID - var err error - if evt.SenderID.Server == types.DefaultUserServer { - senderLID, err = wa.GetStore().LIDs.GetLIDForPN(ctx, evt.SenderID) - if err != nil { - wa.UserLogin.Log.Err(err). - Stringer("sender_id", evt.SenderID). - Msg("Failed to get LID for media retry sender") - return false - } - } - if evt.ChatID.Server == types.DefaultUserServer { - chatLID, err = wa.GetStore().LIDs.GetLIDForPN(ctx, evt.ChatID) - if err != nil { - wa.UserLogin.Log.Err(err). - Stringer("chat_id", evt.ChatID). - Msg("Failed to get LID for media retry chat") - return false - } - } - res := wa.UserLogin.QueueRemoteEvent(&WAMediaRetry{ - MediaRetry: evt, - wa: wa, - senderLID: senderLID, - chatLID: chatLID, - }) - return res.Success -} - func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Receipt) (success bool) { + origChat := evt.Chat + wa.rerouteWAMessage(ctx, "receipt", &evt.MessageSource, evt.MessageIDs) if evt.IsFromMe && evt.Sender.Device == 0 { wa.phoneSeen(evt.Timestamp) } - if !wa.ensureAltJIDs(ctx, &evt.MessageSource, true) { - return false - } var evtType bridgev2.RemoteEventType switch evt.Type { case types.ReceiptTypeRead, types.ReceiptTypeReadSelf: @@ -480,44 +464,29 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei default: return true } - targets := make([]networkid.MessageID, 0, len(evt.MessageIDs)) - messageSender := wa.GetLID() + targets := make([]networkid.MessageID, len(evt.MessageIDs)) + messageSender := wa.JID if !evt.MessageSender.IsEmpty() { messageSender = evt.MessageSender - } else if evt.Chat.Server == types.NewsletterServer { - messageSender = evt.Chat - } - var chatAlt types.JID - if evt.Chat.Server == types.DefaultUserServer { - chatLID, _ := wa.GetStore().LIDs.GetLIDForPN(ctx, evt.Chat) - if !chatLID.IsEmpty() { - chatAlt = evt.Chat - evt.Chat = chatLID + // Second part of rerouting receipts in LID chats + if messageSender == origChat && evt.Chat != origChat { + messageSender = evt.Chat + } + } else if evt.Chat.Server == types.GroupServer && evt.Sender.Server == types.HiddenUserServer { + lid := wa.GetStore().GetLID() + if !lid.IsEmpty() { + messageSender = lid } } - for _, id := range evt.MessageIDs { - targets = append(targets, waid.MakeMessageID(evt.Chat, messageSender, id)) - if !chatAlt.IsEmpty() { - targets = append(targets, waid.MakeMessageID(chatAlt, messageSender, id)) - } - } - senderLID := evt.Sender - if senderLID.Server == types.DefaultUserServer && !evt.SenderAlt.IsEmpty() { - senderLID = evt.SenderAlt - } else if evt.Chat.Server == types.NewsletterServer && evt.Type == types.ReceiptTypeReadSelf { - senderLID = wa.GetLID() + for i, id := range evt.MessageIDs { + targets[i] = waid.MakeMessageID(evt.Chat, messageSender, id) } res := wa.UserLogin.QueueRemoteEvent(&simplevent.Receipt{ EventMeta: simplevent.EventMeta{ Type: evtType, PortalKey: wa.makeWAPortalKey(evt.Chat), - Sender: wa.makeEventSender(ctx, senderLID), + Sender: wa.makeEventSender(ctx, evt.Sender), Timestamp: evt.Timestamp, - LogContext: func(c zerolog.Context) zerolog.Context { - return c. - Strs("targets", exslices.CastToString[string](targets)). - Stringer("receipt_sender", senderLID) - }, }, Targets: targets, }) @@ -525,11 +494,11 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei } func (wa *WhatsAppClient) handleWAChatPresence(ctx context.Context, evt *events.ChatPresence) { - if evt.Chat.Server == types.DefaultUserServer && evt.Sender.ToNonAD() == evt.Chat { + if evt.Chat.Server == types.HiddenUserServer && evt.Sender.ToNonAD() == evt.Chat { if evt.SenderAlt.IsEmpty() { - evt.SenderAlt, _ = wa.GetStore().LIDs.GetLIDForPN(ctx, evt.Sender) + evt.SenderAlt, _ = wa.GetStore().LIDs.GetPNForLID(ctx, evt.Sender) } - if evt.SenderAlt.Server == types.HiddenUserServer { + if evt.SenderAlt.Server == types.DefaultUserServer { evt.Sender, evt.SenderAlt = evt.SenderAlt, evt.Sender evt.Chat = evt.Sender.ToNonAD() } @@ -556,7 +525,7 @@ func (wa *WhatsAppClient) handleWAChatPresence(ctx context.Context, evt *events. }) } -func (wa *WhatsAppClient) handleWALogout(ctx context.Context, reason events.ConnectFailureReason, onConnect bool) { +func (wa *WhatsAppClient) handleWALogout(reason events.ConnectFailureReason, onConnect bool) { errorCode := WAUnknownLogout if reason == events.ConnectFailureLoggedOut { errorCode = WALoggedOut @@ -566,12 +535,7 @@ func (wa *WhatsAppClient) handleWALogout(ctx context.Context, reason events.Conn wa.Disconnect() wa.Client = nil wa.JID = types.EmptyJID - wa.LID = types.EmptyJID wa.UserLogin.Metadata.(*waid.UserLoginMetadata).WADeviceID = 0 - err := wa.Main.DB.Conversation.DeleteAll(ctx, wa.UserLogin.ID) - if err != nil { - zerolog.Ctx(ctx).Err(err).Msg("Failed to delete history sync data on logout") - } wa.UserLogin.BridgeState.Send(status.BridgeState{ StateEvent: status.StateBadCredentials, Error: errorCode, @@ -584,15 +548,12 @@ func (wa *WhatsAppClient) handleWACallStart(ctx context.Context, group, sender, if !wa.Main.Config.CallStartNotices || time.Since(ts) > callEventMaxAge { return true } - if sender.Server == types.DefaultUserServer && senderAlt.IsEmpty() { - senderAlt, _ = wa.GetStore().LIDs.GetLIDForPN(ctx, sender) - } - if sender.Server == types.DefaultUserServer && senderAlt.Server == types.HiddenUserServer { + if sender.Server == types.HiddenUserServer && senderAlt.Server == types.DefaultUserServer { wa.UserLogin.Log.Debug(). - Stringer("lid", senderAlt). - Stringer("pn", sender). + Stringer("lid", sender). + Stringer("pn", senderAlt). Str("call_id", id). - Msg("Forced phone number caller to LID in incoming call") + Msg("Forced LID caller to phone number in incoming call") sender, senderAlt = senderAlt, sender } chat := group @@ -639,12 +600,6 @@ func (wa *WhatsAppClient) handleWAIdentityChange(ctx context.Context, evt *event if !wa.Main.Config.IdentityChangeNotices { return } - if evt.JID.Server == types.DefaultUserServer { - lid, _ := wa.GetStore().LIDs.GetLIDForPN(ctx, evt.JID) - if !lid.IsEmpty() { - evt.JID = lid - } - } wa.UserLogin.QueueRemoteEvent(&simplevent.Message[*events.IdentityChange]{ EventMeta: simplevent.EventMeta{ Type: bridgev2.RemoteEventMessage, @@ -695,14 +650,13 @@ func (wa *WhatsAppClient) handleWADeleteChat(ctx context.Context, evt *events.De func (wa *WhatsAppClient) handleWADeleteForMe(ctx context.Context, evt *events.DeleteForMe) bool { chatJID := wa.maybeConvertJIDToLID(ctx, evt.ChatJID) - senderJID := wa.maybeConvertJIDToLID(ctx, evt.SenderJID) return wa.UserLogin.QueueRemoteEvent(&simplevent.MessageRemove{ EventMeta: simplevent.EventMeta{ Type: bridgev2.RemoteEventMessageRemove, PortalKey: wa.makeWAPortalKey(chatJID), Timestamp: evt.Timestamp, }, - TargetMessage: waid.MakeMessageID(chatJID, senderJID, evt.MessageID), + TargetMessage: waid.MakeMessageID(chatJID, evt.SenderJID, evt.MessageID), OnlyForMe: true, }).Success } @@ -713,7 +667,7 @@ func (wa *WhatsAppClient) handleWAMarkChatAsRead(ctx context.Context, evt *event EventMeta: simplevent.EventMeta{ Type: bridgev2.RemoteEventReadReceipt, PortalKey: wa.makeWAPortalKey(chatJID), - Sender: wa.makeEventSender(ctx, wa.GetLID()), + Sender: wa.makeEventSender(ctx, wa.JID), Timestamp: evt.Timestamp, }, ReadUpTo: evt.Timestamp, @@ -736,13 +690,13 @@ func (wa *WhatsAppClient) syncGhost(jid types.JID, reason string, pictureID *str if pictureID != nil && *pictureID != "" && ghost.AvatarID == networkid.AvatarID(*pictureID) { return } - userInfo, err := wa.getUserInfo(ctx, jid, ptr.Val(pictureID), pictureID != nil) + userInfo, err := wa.getUserInfo(ctx, jid, pictureID != nil) if err != nil { log.Err(err).Msg("Failed to get user info") } else { ghost.UpdateInfo(ctx, userInfo) log.Debug().Msg("Synced ghost info") - wa.syncAltGhostWithInfo(ctx, jid, ghost) + wa.syncAltGhostWithInfo(ctx, jid, userInfo) } go wa.syncRemoteProfile(ctx, ghost) } @@ -802,6 +756,9 @@ func (wa *WhatsAppClient) handleWAGroupInfoChange(ctx context.Context, evt *even } func (wa *WhatsAppClient) handleWAJoinedGroup(ctx context.Context, evt *events.JoinedGroup) bool { + if wa.createDedup.Pop(evt.CreateKey) { + return true + } return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{ EventMeta: simplevent.EventMeta{ Type: bridgev2.RemoteEventChatResync, @@ -814,9 +771,6 @@ func (wa *WhatsAppClient) handleWAJoinedGroup(ctx context.Context, evt *events.J } func (wa *WhatsAppClient) handleWANewsletterJoin(ctx context.Context, evt *events.NewsletterJoin) bool { - if wa.disableNewsletter { - return true - } return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{ EventMeta: simplevent.EventMeta{ Type: bridgev2.RemoteEventChatResync, @@ -915,12 +869,7 @@ func (wa *WhatsAppClient) handleWAAppStateSyncComplete(ctx context.Context, evt } else { log.Info(). Time("recovery_ts", ts). - Bool("recovery_evt", evt.Recovery). Msg("Unmarked app state recovery as attempted after successful full sync") - wa.UserLogin.TrackAnalytics("WhatsApp Appstate Recovery Success", map[string]any{ - "patch_name": evt.Name, - "from_recovery": evt.Recovery, - }) } } else if ts, exists = wa.appStateFullSyncAttempted[evt.Name]; exists { delete(wa.appStateFullSyncAttempted, evt.Name) @@ -977,9 +926,6 @@ func (wa *WhatsAppClient) handleWAAppStateSyncError(ctx context.Context, evt *ev if err != nil { log.Err(err).Msg("Failed to save login metadata after marking app state recovery as attempted") } - wa.UserLogin.TrackAnalytics("WhatsApp Appstate Recovery Request", map[string]any{ - "patch_name": evt.Name, - }) go func() { resp, err := wa.Client.SendPeerMessage(ctx, whatsmeow.BuildAppStateRecoveryRequest(evt.Name)) if err != nil { diff --git a/pkg/connector/id.go b/pkg/connector/id.go index c0aa06b..c07e431 100644 --- a/pkg/connector/id.go +++ b/pkg/connector/id.go @@ -49,7 +49,7 @@ func (wa *WhatsAppClient) makeEventSender(ctx context.Context, id types.JID) bri } } return bridgev2.EventSender{ - IsFromMe: wa.IsOwnJID(id), + IsFromMe: id.User == wa.GetStore().GetJID().User || id.User == wa.GetStore().GetLID().User, Sender: waid.MakeUserID(id), SenderLogin: waid.MakeUserLoginID(senderLoginJID), } @@ -60,25 +60,24 @@ func (wa *WhatsAppClient) messageIDToKey(id *waid.ParsedMessageID) *waCommon.Mes RemoteJID: ptr.Ptr(id.Chat.String()), ID: ptr.Ptr(id.ID), } - if wa.IsOwnJID(id.Sender) { + if id.Sender.User == wa.GetStore().GetJID().User || id.Sender.User == wa.GetStore().GetLID().User { key.FromMe = ptr.Ptr(true) } - if id.Chat.Server != types.MessengerServer && id.Chat.Server != types.DefaultUserServer && - id.Chat.Server != types.HiddenUserServer && id.Chat.Server != types.BotServer { + if id.Chat.Server != types.MessengerServer && id.Chat.Server != types.DefaultUserServer && id.Chat.Server != types.HiddenUserServer && id.Chat.Server != types.BotServer { key.Participant = ptr.Ptr(id.Sender.String()) } return key } -func (wa *WhatsAppClient) maybeConvertJIDToLID(ctx context.Context, jid types.JID) types.JID { - if jid.Server == types.DefaultUserServer { - if lidForPN, err := wa.GetStore().LIDs.GetLIDForPN(ctx, jid); err != nil { +func (wa *WhatsAppClient) maybeConvertJIDToLID(ctx context.Context, chatJID types.JID) types.JID { + if chatJID.Server == types.HiddenUserServer { + if pn, err := wa.GetStore().LIDs.GetPNForLID(ctx, chatJID); err != nil { wa.UserLogin.Log.Err(err). - Stringer("pn", jid). - Msg("Failed to get LID for phone number chat") - } else if !lidForPN.IsEmpty() { - return lidForPN + Stringer("lid", chatJID). + Msg("Failed to get phone number for LID chat") + } else if !pn.IsEmpty() { + return pn.ToNonAD() } } - return jid + return chatJID } diff --git a/pkg/connector/lidmigrate.go b/pkg/connector/lidmigrate.go deleted file mode 100644 index ec1c700..0000000 --- a/pkg/connector/lidmigrate.go +++ /dev/null @@ -1,269 +0,0 @@ -// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. -// Copyright (C) 2026 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package connector - -import ( - "context" - "fmt" - "time" - - "github.com/rs/zerolog" - "go.mau.fi/util/dbutil" - "go.mau.fi/whatsmeow/types" - - "maunium.net/go/mautrix/bridgev2" - "maunium.net/go/mautrix/bridgev2/networkid" - "maunium.net/go/mautrix/event" - - "go.mau.fi/mautrix-whatsapp/pkg/waid" -) - -func (wa *WhatsAppClient) FindAltTargetMessage(ctx context.Context, targetMsg networkid.MessageID, evt bridgev2.RemoteEventWithTargetMessage) (alts []networkid.MessageID, err error) { - parsed, err := waid.ParseMessageID(targetMsg) - if err != nil { - return nil, fmt.Errorf("failed to parse target message ID: %w", err) - } - altSender, err := wa.GetStore().GetAltJID(ctx, parsed.Sender) - if err != nil { - return nil, err - } - var altChat types.JID - if parsed.Chat.Server == types.HiddenUserServer { - altChat, err = wa.GetStore().LIDs.GetPNForLID(ctx, parsed.Chat) - if err != nil { - return nil, err - } - } - if !altSender.IsEmpty() { - altSenderID := *parsed - altSenderID.Sender = altSender - alts = append(alts, altSenderID.String()) - } - if !altChat.IsEmpty() { - altChatID := *parsed - altChatID.Chat = altChat - if altSender.Server == types.DefaultUserServer { - altChatID.Sender = altSender - } - alts = append(alts, altChatID.String()) - } - return -} - -func (wa *WhatsAppClient) checkAllPhonesInMessage(ctx context.Context, info *types.MessageSource) (ok bool) { - for _, jid := range []types.JID{info.Sender, info.SenderAlt, info.Chat, info.RecipientAlt, info.BroadcastListOwner} { - if !wa.reIDPhoneDMToLIDIfNeeded(ctx, jid) { - return false - } - } - return true -} - -func (wa *WhatsAppClient) reIDPhoneDMToLIDIfNeeded(ctx context.Context, pn types.JID) (ok bool) { - if pn.Server != types.DefaultUserServer { - return true - } - portalKey := wa.makeWAPortalKey(pn) - if wa.Main.unmigratedDMs.Has(portalKey) { - lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, pn) - if err != nil { - zerolog.Ctx(ctx).Err(err).Stringer("pn", pn).Msg("Failed to get LID for PN") - return false - } else if lid.IsEmpty() { - zerolog.Ctx(ctx).Warn().Stringer("pn", pn).Msg("No found LID for phone number") - return true - } - zerolog.Ctx(ctx).Info(). - Object("portal_key", portalKey). - Stringer("pn", pn). - Stringer("lid", lid). - Msg("Received event for portal in unmigrated DMs list, trying migration") - _, err = wa.Main.reIDPhoneDMToLID(ctx, pn, lid, wa.UserLogin.ID) - if err != nil { - zerolog.Ctx(ctx).Err(err).Msg("Failed to re-ID phone DM to LID") - return false - } - } - return true -} - -func (wa *WhatsAppConnector) reIDPhoneDMToLID(ctx context.Context, pn, lid types.JID, receiver networkid.UserLoginID) (bridgev2.ReIDResult, error) { - pnKey := networkid.PortalKey{ - ID: waid.MakePortalID(pn), - Receiver: receiver, - } - lidKey := networkid.PortalKey{ - ID: waid.MakePortalID(lid), - Receiver: receiver, - } - result, portal, err := wa.Bridge.ReIDPortal(ctx, pnKey, lidKey) - if err != nil { - return result, err - } - if result == bridgev2.ReIDResultSourceReIDd || result == bridgev2.ReIDResultTargetDeletedAndSourceReIDd { - var pnGhost, lidGhost *bridgev2.Ghost - pnGhost, err = wa.Bridge.GetGhostByID(ctx, waid.MakeUserID(pn)) - if err != nil { - return result, fmt.Errorf("failed to get PN ghost: %w", err) - } - lidGhost, err = wa.Bridge.GetGhostByID(ctx, waid.MakeUserID(lid)) - if err != nil { - return result, fmt.Errorf("failed to get LID ghost: %w", err) - } - _, err = pnGhost.Intent.SendState(ctx, portal.MXID, event.StateMember, pnGhost.Intent.GetMXID().String(), &event.Content{ - Parsed: &event.MemberEventContent{Membership: event.MembershipLeave, Reason: "Migrating to LIDs"}, - Raw: map[string]any{"com.beeper.exclude_from_timeline": true}, - }, time.Time{}) - if err != nil { - return result, fmt.Errorf("failed to send leave event for PN ghost: %w", err) - } - _, err = wa.Bridge.Bot.SendState(ctx, portal.MXID, event.StateMember, lidGhost.Intent.GetMXID().String(), &event.Content{ - Parsed: &event.MemberEventContent{Membership: event.MembershipInvite, Reason: "Migrating to LIDs"}, - Raw: map[string]any{"com.beeper.exclude_from_timeline": true}, - }, time.Time{}) - if err != nil { - return result, fmt.Errorf("failed to send invite event for LID ghost: %w", err) - } - _, err = lidGhost.Intent.SendState(ctx, portal.MXID, event.StateMember, lidGhost.Intent.GetMXID().String(), &event.Content{ - Parsed: &event.MemberEventContent{Membership: event.MembershipJoin, Reason: "Migrating to LIDs"}, - Raw: map[string]any{"com.beeper.exclude_from_timeline": true}, - }, time.Time{}) - if err != nil { - return result, fmt.Errorf("failed to send join event for LID ghost: %w", err) - } - portal.OtherUserID = lidGhost.ID - err = portal.Save(ctx) - if err != nil { - return result, fmt.Errorf("failed to save portal after re-ID: %w", err) - } - portal.UpdateBridgeInfo(ctx) - } - return result, nil -} - -var scanPortalKey = dbutil.ConvertRowFn[networkid.PortalKey](func(row dbutil.Scannable) (key networkid.PortalKey, err error) { - err = row.Scan(&key.ID, &key.Receiver) - return -}) - -func (wa *WhatsAppConnector) migrateToLIDDMs(ctx context.Context) error { - if wa.Bridge.Background { - if wa.Bridge.DB.KV.Get(ctx, "whatsapp_lid_dms_migrated") == "true" { - return nil - } - return fmt.Errorf("can't migrate to LID DMs in background mode") - } - log := zerolog.Ctx(ctx).With().Str("action", "migrate to lid dms").Logger() - const findPNPortals = "SELECT id, receiver FROM portal WHERE bridge_id=$1 AND room_type='dm' AND id LIKE '%@s.whatsapp.net'" - pnPortalKeys, err := scanPortalKey.NewRowIter(wa.Bridge.DB.Query(ctx, findPNPortals, wa.Bridge.ID)).AsList() - if err != nil { - return fmt.Errorf("failed to get phone number portals: %w", err) - } - var updatedPortals, missingLID int - for _, key := range pnPortalKeys { - pnJID, err := waid.ParsePortalID(key.ID) - if err != nil { - log.Warn().Err(err).Str("portal_id", string(key.ID)).Msg("Failed to parse portal ID") - continue - } else if pnJID.Server != types.DefaultUserServer { - continue - } - lid, err := wa.DeviceStore.LIDMap.GetLIDForPN(ctx, pnJID) - if err != nil { - return fmt.Errorf("failed to get LID for PN portal %s: %w", key.ID, err) - } else if lid.IsEmpty() { - log.Warn().Stringer("pn", pnJID).Msg("No LID for PN portal") - wa.unmigratedDMs.Add(key) - missingLID++ - continue - } - res, err := wa.reIDPhoneDMToLID(ctx, pnJID, lid, key.Receiver) - if err != nil { - return fmt.Errorf("failed to re-ID %s to %s: %w", pnJID, lid, err) - } - updatedPortals++ - log.Info(). - Stringer("pn", pnJID). - Stringer("lid", lid). - Stringer("result", res). - Msg("Re-ID'd phone number DM portal") - } - log.Info(). - Int("updated_portals", updatedPortals). - Int("total_pn_portals", len(pnPortalKeys)). - Int("missing_lid", missingLID). - Msg("Finished re-IDing phone number DM portals") - wa.Bridge.DB.KV.Set(ctx, "whatsapp_lid_dms_migrated", "true") - return nil -} - -func (wa *WhatsAppConnector) syncMismatchingGhosts(ctx context.Context) error { - if wa.Bridge.Background || wa.Bridge.DB.KV.Get(ctx, "whatsapp_lid_avatars_resynced") == "true" { - return nil - } - const findMismatchingGhosts = ` - SELECT id - FROM ghost - WHERE bridge_id=$1 - AND id LIKE 'lid-%' - AND avatar_mxc='' - AND EXISTS ( - SELECT pnghost.avatar_mxc - FROM ghost pnghost - WHERE pnghost.bridge_id=$1 - AND pnghost.id=(SELECT pn FROM whatsmeow_lid_map WHERE lid=replace(ghost.id, 'lid-', '')) - AND pnghost.avatar_mxc<>'' - ) - ` - var scanGhostID = dbutil.ConvertRowFn[networkid.UserID](dbutil.ScanSingleColumn[networkid.UserID]) - ghostIDs, err := scanGhostID.NewRowIter(wa.Bridge.DB.Query(ctx, findMismatchingGhosts, wa.Bridge.ID)).AsList() - if err != nil { - return fmt.Errorf("failed to get mismatching ghosts: %w", err) - } - for _, ghostID := range ghostIDs { - lid := waid.ParseUserID(ghostID) - pn, err := wa.DeviceStore.LIDMap.GetPNForLID(ctx, lid) - if err != nil { - return fmt.Errorf("failed to get PN for LID %s: %w", lid, err) - } else if pn.IsEmpty() { - zerolog.Ctx(ctx).Warn().Stringer("lid", lid).Msg("No PN for LID") - continue - } - pnGhost, err := wa.Bridge.GetGhostByID(ctx, waid.MakeUserID(pn)) - if err != nil { - return fmt.Errorf("failed to get PN ghost for %s: %w", pn, err) - } - lidGhost, err := wa.Bridge.GetGhostByID(ctx, ghostID) - if err != nil { - return fmt.Errorf("failed to get LID ghost for %s: %w", ghostID, err) - } - if lidGhost.AvatarMXC != "" || pnGhost.AvatarMXC == "" { - continue - } - zerolog.Ctx(ctx).Debug(). - Stringer("pn", pn). - Stringer("lid", lid). - Str("pn_ghost_avatar", string(pnGhost.AvatarMXC)). - Str("lid_ghost_avatar", string(lidGhost.AvatarMXC)). - Str("pn_ghost_name", pnGhost.Name). - Str("lid_ghost_name", lidGhost.Name). - Msg("Updating LID ghost avatar") - lidGhost.UpdateInfo(ctx, makeInfoFromGhost(pnGhost)) - } - wa.Bridge.DB.KV.Set(ctx, "whatsapp_lid_avatars_resynced", "true") - return nil -} diff --git a/pkg/connector/login.go b/pkg/connector/login.go index 80a6abb..5da419f 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "net/http" - "strings" "sync/atomic" "time" @@ -99,7 +98,6 @@ func (wa *WhatsAppConnector) CreateLogin(_ context.Context, user *bridgev2.User, LoginComplete: exsync.NewEvent(), PasskeyRequest: exsync.NewEvent(), PasskeyConfirmation: exsync.NewEvent(), - ADVRotate: exsync.NewEvent(), Received515: exsync.NewEvent(), }, nil } @@ -126,8 +124,6 @@ type WALogin struct { PasskeyConfirmation *exsync.Event PasskeyConfirmationData *events.PairPasskeyConfirmation - ADVRotate *exsync.Event - Closed atomic.Bool EventHandlerID uint32 } @@ -139,8 +135,7 @@ var ( _ bridgev2.LoginProcessWebAuthn = (*WALogin)(nil) ) -const LoginConnectWait = 30 * time.Second -const LoginPairPhoneWait = 30 * time.Second +const LoginConnectWait = 15 * time.Second func (wl *WALogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) { wl.Main.firstClientConnectOnce.Do(wl.Main.onFirstClientConnect) @@ -198,21 +193,19 @@ func (wl *WALogin) StartWithOverride(ctx context.Context, old *bridgev2.UserLogi } func (wl *WALogin) SubmitUserInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) { + ctx, cancel := context.WithTimeout(ctx, LoginConnectWait) + defer cancel() err := wl.Client.Connect() if err != nil { wl.Log.Err(err).Msg("Failed to connect to WhatsApp for phone code login") return nil, err } - connectCtx, cancelConnect := context.WithTimeout(ctx, LoginConnectWait) - err = wl.WaitForQRs.Wait(connectCtx) - cancelConnect() + err = wl.WaitForQRs.Wait(ctx) if err != nil { wl.Log.Warn().Err(err).Msg("Timed out waiting for connection") return nil, fmt.Errorf("failed to wait for connection: %w", err) } - pairCtx, cancelPair := context.WithTimeout(ctx, LoginPairPhoneWait) - defer cancelPair() - pairingCode, err := wl.Client.PairPhone(pairCtx, input["phone_number"], true, whatsmeow.PairClientChrome, "Chrome (Linux)") + pairingCode, err := wl.Client.PairPhone(ctx, input["phone_number"], true, whatsmeow.PairClientChrome, "Chrome (Linux)") if err != nil { wl.Log.Err(err).Msg("Failed to request phone code login") if errors.Is(err, whatsmeow.ErrPhoneNumberTooShort) { @@ -274,12 +267,6 @@ func (wl *WALogin) handleEvent(rawEvt any) { wl.StartTime = time.Now() wl.WaitForQRs.Set() return - case *events.RotateADVSecret: - wl.Log.Debug().Msg("Rotating ADV secret in all QRs") - for i, code := range wl.QRs { - wl.QRs[i] = strings.Replace(code, evt.OldSecret, evt.NewSecret, 1) - } - wl.ADVRotate.Set() case *events.QRScannedWithoutMultidevice: wl.Log.Error().Msg("QR code scanned without multidevice enabled") wl.LoginError = ErrLoginMultideviceNotEnabled @@ -342,7 +329,6 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) { Int("current_index", currentIndex) if currentIndex > prevIndex { logEvt.Msg("Returning new QR immediately") - wl.ADVRotate.Clear() wl.PrevQRIndex.Store(int32(currentIndex)) return makeQRStep(wl.QRs[currentIndex]), nil } @@ -354,7 +340,6 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) { wl.Cancel() return nil, ErrLoginTimeout } - wl.ADVRotate.Clear() wl.PrevQRIndex.Store(int32(nextIndex)) return makeQRStep(wl.QRs[nextIndex]), nil case <-ctx.Done(): @@ -362,10 +347,6 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) { return nil, ctx.Err() case <-wl.PasskeyRequest.GetChan(): return wl.makePasskeyStep() - case <-wl.ADVRotate.GetChan(): - wl.Log.Debug().Msg("ADV secret was rotated, returning new QR immediately") - wl.ADVRotate.Clear() - return makeQRStep(wl.QRs[nextIndex]), nil case <-wl.LoginComplete.GetChan(): // continue } diff --git a/pkg/connector/mediarequest.go b/pkg/connector/mediarequest.go index 3806196..2686250 100644 --- a/pkg/connector/mediarequest.go +++ b/pkg/connector/mediarequest.go @@ -155,8 +155,8 @@ func (wa *WhatsAppClient) sendMediaRequestDirect(ctx context.Context, rawMsgID n return wa.Client.SendMediaRetryReceipt(ctx, &types.MessageInfo{ ID: msgID.ID, MessageSource: types.MessageSource{ - IsFromMe: wa.IsOwnJID(msgID.Sender), - IsGroup: msgID.Chat.Server != types.DefaultUserServer && msgID.Chat.Server != types.HiddenUserServer && msgID.Chat.Server != types.BotServer, + IsFromMe: msgID.Sender.User == wa.JID.User, + IsGroup: msgID.Chat.Server != types.DefaultUserServer && msgID.Chat.Server != types.BotServer, Sender: msgID.Sender, Chat: msgID.Chat, }, diff --git a/pkg/connector/startchat.go b/pkg/connector/startchat.go index 18e8326..fb2ce16 100644 --- a/pkg/connector/startchat.go +++ b/pkg/connector/startchat.go @@ -25,7 +25,6 @@ import ( "time" "github.com/rs/zerolog" - "go.mau.fi/util/exmaps" "go.mau.fi/util/exsync" "go.mau.fi/util/ptr" "go.mau.fi/whatsmeow" @@ -64,12 +63,12 @@ func looksEmaily(str string) bool { return false } -type isOnWhatsappCacheEntry struct { +type cacheEntry struct { jid types.JID ts time.Time } -var isOnWhatsappCache = exsync.NewMap[string, isOnWhatsappCacheEntry]() +var isOnWhatsappCache = exsync.NewMap[string, cacheEntry]() func (wa *WhatsAppClient) validateIdentifer(ctx context.Context, number string) (types.JID, error) { if strings.HasSuffix(number, "@"+types.BotServer) || strings.HasSuffix(number, "@"+types.HiddenUserServer) { @@ -94,7 +93,7 @@ func (wa *WhatsAppClient) validateIdentifer(ctx context.Context, number string) } else if !resp[0].IsIn { return types.EmptyJID, bridgev2.WrapRespErr(fmt.Errorf("the server said +%s is not on WhatsApp", resp[0].JID.User), mautrix.MNotFound) } else { - isOnWhatsappCache.Set(number, isOnWhatsappCacheEntry{resp[0].JID, time.Now()}) + isOnWhatsappCache.Set(number, cacheEntry{resp[0].JID, time.Now()}) return resp[0].JID, nil } } @@ -120,24 +119,16 @@ func (wa *WhatsAppConnector) ValidateUserID(id networkid.UserID) bool { } } -func (wa *WhatsAppClient) startChatPNToLID(ctx context.Context, jid types.JID) (types.JID, error) { - if jid.Server == types.DefaultUserServer { - lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, jid) +func (wa *WhatsAppClient) startChatLIDToPN(ctx context.Context, jid types.JID) (types.JID, error) { + if jid.Server == types.HiddenUserServer { + pn, err := wa.GetStore().LIDs.GetPNForLID(ctx, jid) if err != nil { - return jid, fmt.Errorf("failed to get lid for phone number: %w", err) - } else if lid.IsEmpty() { - resp, err := wa.Client.GetUserInfo(ctx, []types.JID{jid}) - if err != nil { - return jid, fmt.Errorf("failed to get user info for phone number: %w", err) - } else if info, ok := resp[jid]; !ok { - return jid, fmt.Errorf("server didn't return user info for phone number") - } else if info.LID.IsEmpty() { - return jid, fmt.Errorf("server didn't return lid for phone number") - } else { - return info.LID, nil - } + return jid, fmt.Errorf("failed to get phone number for lid: %w", err) + } else if pn.IsEmpty() { + // Don't allow starting chats with LIDs for now + return jid, fmt.Errorf("phone number not found") } - return lid, nil + return pn, nil } return jid, nil } @@ -156,7 +147,7 @@ func (wa *WhatsAppClient) makeCreateChatResponse(ctx context.Context, jid, origJ func (wa *WhatsAppClient) CreateChatWithGhost(ctx context.Context, ghost *bridgev2.Ghost) (*bridgev2.CreateChatResponse, error) { origJID := waid.ParseUserID(ghost.ID) - jid, err := wa.startChatPNToLID(ctx, origJID) + jid, err := wa.startChatLIDToPN(ctx, origJID) if err != nil { return nil, err } @@ -168,7 +159,7 @@ func (wa *WhatsAppClient) ResolveIdentifier(ctx context.Context, identifier stri if err != nil { return nil, err } - jid, err := wa.startChatPNToLID(ctx, origJID) + jid, err := wa.startChatLIDToPN(ctx, origJID) if err != nil { return nil, err } @@ -176,16 +167,11 @@ func (wa *WhatsAppClient) ResolveIdentifier(ctx context.Context, identifier stri if err != nil { return nil, fmt.Errorf("failed to get ghost: %w", err) } - userInfo, err := wa.getUserInfo(ctx, jid, "", false) - if err != nil { - return nil, fmt.Errorf("failed to get user info: %w", err) - } return &bridgev2.ResolveIdentifierResponse{ - Ghost: ghost, - UserID: waid.MakeUserID(jid), - UserInfo: userInfo, - Chat: wa.makeCreateChatResponse(ctx, jid, origJID), + Ghost: ghost, + UserID: waid.MakeUserID(jid), + Chat: wa.makeCreateChatResponse(ctx, jid, origJID), }, nil } @@ -213,7 +199,6 @@ func (wa *WhatsAppClient) getContactList(ctx context.Context, filter string, onl return nil, err } resp := make([]*bridgev2.ResolveIdentifierResponse, 0, len(contacts)) - addedIDs := make(exmaps.Set[types.JID]) for jid, contactInfo := range contacts { if onlyContacts && (contactInfo.FirstName == "" && contactInfo.FullName == "") { continue @@ -221,43 +206,31 @@ func (wa *WhatsAppClient) getContactList(ctx context.Context, filter string, onl if !matchesQuery(contactInfo.PushName, filter) && !matchesQuery(contactInfo.FullName, filter) && !matchesQuery(jid.User, filter) { continue } - var lid types.JID - if jid.Server == types.HiddenUserServer { - lid = jid - } else if jid.Server == types.DefaultUserServer { - lid, err = wa.GetStore().LIDs.GetLIDForPN(ctx, jid) - if err != nil { - return nil, fmt.Errorf("failed to get lid for phone number %s: %w", jid, err) - } else if !lid.IsEmpty() { - jid = lid - } - } - if !addedIDs.Add(jid) { - continue - } - var chatResp *bridgev2.CreateChatResponse - if !lid.IsEmpty() { - chatResp = &bridgev2.CreateChatResponse{PortalKey: wa.makeWAPortalKey(lid)} - } ghost, _ := wa.Main.Bridge.GetGhostByID(ctx, waid.MakeUserID(jid)) resp = append(resp, &bridgev2.ResolveIdentifierResponse{ Ghost: ghost, UserID: waid.MakeUserID(jid), - UserInfo: wa.contactToUserInfo(ctx, jid, contactInfo, "", false), - Chat: chatResp, + UserInfo: wa.contactToUserInfo(ctx, jid, contactInfo, false), + Chat: &bridgev2.CreateChatResponse{PortalKey: wa.makeWAPortalKey(jid)}, }) } return resp, nil } func (wa *WhatsAppClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCreateParams) (*bridgev2.CreateChatResponse, error) { + createKey := wa.Client.GenerateMessageID() + if params.RoomID != "" { + wa.createDedup.Add(createKey) + } req := whatsmeow.ReqCreateGroup{ Name: ptr.Val(params.Name).Name, Participants: make([]types.JID, len(params.Participants)), + CreateKey: createKey, } for i, participant := range params.Participants { jid := waid.ParseUserID(participant) - jid, err := wa.startChatPNToLID(ctx, jid) + // Normalize to PN if it's a LID + jid, err := wa.startChatLIDToPN(ctx, jid) if err != nil { return nil, fmt.Errorf("failed to normalize participant %s: %w", participant, err) } diff --git a/pkg/connector/userinfo.go b/pkg/connector/userinfo.go index c78b0a8..e827955 100644 --- a/pkg/connector/userinfo.go +++ b/pkg/connector/userinfo.go @@ -165,13 +165,13 @@ func (wa *WhatsAppClient) doGhostResync(ctx context.Context, queue map[types.JID log.Warn().Stringer("jid", jid).Msg("Didn't get info for puppet in background sync") continue } - userInfo, err := wa.getUserInfo(ctx, jid, info.PictureID, info.PictureID != "" && string(ghost.AvatarID) != info.PictureID) + userInfo, err := wa.getUserInfo(ctx, jid, info.PictureID != "" && string(ghost.AvatarID) != info.PictureID) if err != nil { log.Err(err).Stringer("jid", jid).Msg("Failed to get user info for puppet in background sync") continue } ghost.UpdateInfo(ctx, userInfo) - wa.syncAltGhostWithInfo(ctx, jid, ghost) + wa.syncAltGhostWithInfo(ctx, jid, userInfo) } } @@ -181,18 +181,18 @@ func (wa *WhatsAppClient) GetUserInfo(ctx context.Context, ghost *bridgev2.Ghost return nil, nil } jid := waid.ParseUserID(ghost.ID) - return wa.getUserInfo(ctx, jid, "", ghost.AvatarID == "") + return wa.getUserInfo(ctx, jid, ghost.AvatarID == "") } -func (wa *WhatsAppClient) getUserInfo(ctx context.Context, jid types.JID, avatarID string, fetchAvatar bool) (*bridgev2.UserInfo, error) { +func (wa *WhatsAppClient) getUserInfo(ctx context.Context, jid types.JID, fetchAvatar bool) (*bridgev2.UserInfo, error) { contact, err := wa.GetStore().Contacts.GetContact(ctx, jid) if err != nil { return nil, err } - return wa.contactToUserInfo(ctx, jid, contact, avatarID, fetchAvatar), nil + return wa.contactToUserInfo(ctx, jid, contact, fetchAvatar), nil } -func (wa *WhatsAppClient) contactToUserInfo(ctx context.Context, jid types.JID, contact types.ContactInfo, avatarID string, fetchAvatar bool) *bridgev2.UserInfo { +func (wa *WhatsAppClient) contactToUserInfo(ctx context.Context, jid types.JID, contact types.ContactInfo, getAvatar bool) *bridgev2.UserInfo { if jid == types.MetaAIJID && contact.PushName == jid.User { contact.PushName = "Meta AI" } else if jid == types.LegacyPSAJID || jid == types.PSAJID { @@ -270,9 +270,7 @@ func (wa *WhatsAppClient) contactToUserInfo(ctx context.Context, jid types.JID, } else if phone != "" { ui.Identifiers = []string{fmt.Sprintf("tel:%s", phone)} } - if wa.Main.Config.LazyAvatars { - ui.ExtraUpdates = bridgev2.MergeExtraUpdaters(ui.ExtraUpdates, wa.makeLazyGhostAvatarUpdater(avatarID, fetchAvatar)) - } else if fetchAvatar { + if getAvatar { ui.ExtraUpdates = bridgev2.MergeExtraUpdaters(ui.ExtraUpdates, wa.fetchGhostAvatar) } return ui @@ -309,21 +307,6 @@ func avatarInfoToCacheEntry(ctx context.Context, jid types.JID, avatar *types.Pr } } -func (wa *WhatsAppClient) makeLazyDirectMediaAvatar(ctx context.Context, jid types.JID, avatarID string, community bool) (*bridgev2.Avatar, error) { - if avatarID == "" { - avatarID = waid.MakeRandomAvatarID() - } - mxc, err := wa.Main.Bridge.Matrix.GenerateContentURI(ctx, waid.MakeAvatarMediaID(jid, avatarID, wa.UserLogin.ID, community)) - if err != nil { - return nil, fmt.Errorf("failed to generate MXC URI: %w", err) - } - return &bridgev2.Avatar{ - ID: networkid.AvatarID(avatarID), - MXC: mxc, - Hash: sha256.Sum256([]byte(avatarID)), - }, nil -} - func (wa *WhatsAppClient) makeDirectMediaAvatar(ctx context.Context, jid types.JID, avatar *types.ProfilePictureInfo, community bool) (*bridgev2.Avatar, error) { mxc, err := wa.Main.Bridge.Matrix.GenerateContentURI(ctx, waid.MakeAvatarMediaID(jid, avatar.ID, wa.UserLogin.ID, community)) if err != nil { @@ -345,21 +328,6 @@ func (wa *WhatsAppClient) makeDirectMediaAvatar(ctx context.Context, jid types.J }, nil } -func (wa *WhatsAppClient) makeLazyGhostAvatarUpdater(avatarID string, forceUpdate bool) func(context.Context, *bridgev2.Ghost) bool { - return func(ctx context.Context, ghost *bridgev2.Ghost) bool { - if ghost.AvatarID != "" && (networkid.AvatarID(avatarID) == ghost.AvatarID || (!forceUpdate && avatarID == "")) { - return false - } - jid := waid.ParseUserID(ghost.ID) - wrappedAvatar, err := wa.makeLazyDirectMediaAvatar(ctx, jid, avatarID, false) - if err != nil { - zerolog.Ctx(ctx).Err(err).Msg("Failed to prepare lazy direct media avatar") - return false - } - return ghost.UpdateAvatar(ctx, wrappedAvatar) - } -} - func (wa *WhatsAppClient) fetchGhostAvatar(ctx context.Context, ghost *bridgev2.Ghost) bool { jid := waid.ParseUserID(ghost.ID) existingID := string(ghost.AvatarID) @@ -431,14 +399,14 @@ func (wa *WhatsAppClient) resyncContacts(forceAvatarSync, automatic bool) { } else if contact, err := contactStore.GetContact(ctx, jid); err != nil { log.Err(err).Stringer("jid", jid).Msg("Failed to get contact info") } else { - userInfo := wa.contactToUserInfo(ctx, jid, contact, "", forceAvatarSync || ghost.AvatarID == "") + userInfo := wa.contactToUserInfo(ctx, jid, contact, forceAvatarSync || ghost.AvatarID == "") ghost.UpdateInfo(ctx, userInfo) - wa.syncAltGhostWithInfo(ctx, jid, ghost) + wa.syncAltGhostWithInfo(ctx, jid, userInfo) } } } -func (wa *WhatsAppClient) syncAltGhostWithInfo(ctx context.Context, jid types.JID, mainGhost *bridgev2.Ghost) { +func (wa *WhatsAppClient) syncAltGhostWithInfo(ctx context.Context, jid types.JID, info *bridgev2.UserInfo) { log := zerolog.Ctx(ctx) var altJID types.JID var err error @@ -463,25 +431,10 @@ func (wa *WhatsAppClient) syncAltGhostWithInfo(ctx context.Context, jid types.JI Msg("Failed to get ghost for alternate JID") return } - ghost.UpdateInfo(ctx, makeInfoFromGhost(mainGhost)) + ghost.UpdateInfo(ctx, info) log.Debug(). Stringer("jid", jid). Stringer("alternate_jid", altJID). Msg("Synced alternate ghost with info") go wa.syncRemoteProfile(ctx, ghost) } - -func makeInfoFromGhost(ghost *bridgev2.Ghost) *bridgev2.UserInfo { - return &bridgev2.UserInfo{ - Identifiers: ghost.Identifiers, - Name: &ghost.Name, - Avatar: &bridgev2.Avatar{ - ID: ghost.AvatarID, - Remove: ghost.AvatarID == "" || ghost.AvatarMXC == "", - MXC: ghost.AvatarMXC, - Hash: ghost.AvatarHash, - }, - IsBot: &ghost.IsBot, - ExtraProfile: ghost.ExtraProfile, - } -} diff --git a/pkg/connector/wadb/avatarcache.go b/pkg/connector/wadb/avatarcache.go index 55fb4b9..8ea947f 100644 --- a/pkg/connector/wadb/avatarcache.go +++ b/pkg/connector/wadb/avatarcache.go @@ -2,14 +2,10 @@ package wadb import ( "context" - "strings" - "time" "go.mau.fi/util/dbutil" "go.mau.fi/util/jsontime" "go.mau.fi/whatsmeow/types" - - "go.mau.fi/mautrix-whatsapp/pkg/waid" ) type AvatarCacheQuery struct { @@ -53,13 +49,3 @@ func (ace *AvatarCacheEntry) Scan(row dbutil.Scannable) (*AvatarCacheEntry, erro func (ace *AvatarCacheEntry) sqlVariables() []any { return []any{ace.EntityJID, ace.AvatarID, ace.DirectPath, ace.Expiry, ace.Gone} } - -func (ace *AvatarCacheEntry) IsGone() bool { - return ace != nil && ace.Gone && - // Random IDs can be retried after a specific expiry time, other types of gones can't - (ace.Expiry.IsZero() || !strings.HasPrefix(ace.AvatarID, waid.RandomAvatarIDPrefix) || ace.Expiry.After(time.Now())) -} - -func (ace *AvatarCacheEntry) Expired() bool { - return ace == nil || ace.Expiry.Before(time.Now().Add(5*time.Minute)) -} diff --git a/pkg/connector/wadb/message.go b/pkg/connector/wadb/message.go index 7a6c63d..4b16002 100644 --- a/pkg/connector/wadb/message.go +++ b/pkg/connector/wadb/message.go @@ -96,12 +96,12 @@ func (mq *MessageQuery) GetBetween(ctx context.Context, loginID networkid.UserLo args := []any{mq.BridgeID, loginID, chatJID} argNum := 4 if startTime != nil { - whereClauses += fmt.Sprintf(" AND timestamp > $%d", argNum) + whereClauses += fmt.Sprintf(" AND timestamp >= $%d", argNum) args = append(args, startTime.Unix()) argNum++ } if endTime != nil { - whereClauses += fmt.Sprintf(" AND timestamp < $%d", argNum) + whereClauses += fmt.Sprintf(" AND timestamp <= $%d", argNum) args = append(args, endTime.Unix()) } diff --git a/pkg/connector/wadb/upgrades/00-latest-schema.sql b/pkg/connector/wadb/upgrades/00-latest-schema.sql index 0f1fef9..850f5b4 100644 --- a/pkg/connector/wadb/upgrades/00-latest-schema.sql +++ b/pkg/connector/wadb/upgrades/00-latest-schema.sql @@ -1,4 +1,4 @@ --- v0 -> v10 (compatible with v3+): Latest revision +-- v0 -> v9 (compatible with v3+): Latest revision CREATE TABLE whatsapp_poll_option_id ( bridge_id TEXT NOT NULL, diff --git a/pkg/connector/wadb/upgrades/08-may-need-lid-dm-deletion.sql b/pkg/connector/wadb/upgrades/08-may-need-lid-dm-deletion.sql new file mode 100644 index 0000000..d5b4ca0 --- /dev/null +++ b/pkg/connector/wadb/upgrades/08-may-need-lid-dm-deletion.sql @@ -0,0 +1,2 @@ +-- v8 (compatible with v3+): Mark LID DMs for deletion +INSERT INTO kv_store (bridge_id, key, value) VALUES ('', 'whatsapp_lid_dms_deleted', 'false'); diff --git a/pkg/connector/wadb/upgrades/09-may-need-lid-dm-deletion-again.sql b/pkg/connector/wadb/upgrades/09-may-need-lid-dm-deletion-again.sql new file mode 100644 index 0000000..5c32e65 --- /dev/null +++ b/pkg/connector/wadb/upgrades/09-may-need-lid-dm-deletion-again.sql @@ -0,0 +1,3 @@ +-- v9 (compatible with v3+): Mark LID DMs for deletion (again) +DELETE FROM kv_store WHERE bridge_id='' AND key='whatsapp_lid_dms_deleted'; +INSERT INTO kv_store (bridge_id, key, value) VALUES ('', 'whatsapp_lid_dms_deleted', 'false'); diff --git a/pkg/connector/wadb/upgrades/10-history-sync-conversation-to-lid.sql b/pkg/connector/wadb/upgrades/10-history-sync-conversation-to-lid.sql deleted file mode 100644 index 334c597..0000000 --- a/pkg/connector/wadb/upgrades/10-history-sync-conversation-to-lid.sql +++ /dev/null @@ -1,22 +0,0 @@ --- v10 (compatible with v3+): Move history sync conversations to LIDs - --- Delete history sync conversations where a @lid conversation already exists -DELETE FROM whatsapp_history_sync_conversation -WHERE chat_jid LIKE '%@lid' AND EXISTS ( - SELECT 1 - FROM whatsapp_history_sync_conversation pnconv - WHERE pnconv.chat_jid=( - SELECT pn || '@s.whatsapp.net' - FROM whatsmeow_lid_map - WHERE lid=replace(whatsapp_history_sync_conversation.chat_jid, '@lid', '') - ) -); - --- Update all phone number conversations to lids if the lid is known -UPDATE whatsapp_history_sync_conversation -SET chat_jid=(SELECT lid || '@lid' FROM whatsmeow_lid_map WHERE pn=replace(chat_jid, '@s.whatsapp.net', '')) -WHERE chat_jid LIKE '%@s.whatsapp.net' - AND EXISTS (SELECT 1 FROM whatsmeow_lid_map WHERE pn=replace(chat_jid, '@s.whatsapp.net', '')); - --- Delete blank phone number portals -DELETE FROM portal WHERE id LIKE '%@s.whatsapp.net' AND (mxid IS NULL OR mxid='') AND room_type=''; diff --git a/pkg/connector/wadb/upgrades/upgrades.go b/pkg/connector/wadb/upgrades/upgrades.go index 5547982..22c5414 100644 --- a/pkg/connector/wadb/upgrades/upgrades.go +++ b/pkg/connector/wadb/upgrades/upgrades.go @@ -6,9 +6,11 @@ import ( "go.mau.fi/util/dbutil" ) +var Table dbutil.UpgradeTable + //go:embed *.sql var rawUpgrades embed.FS -var Table = dbutil.BuildUpgradeTable(). - WithFS(rawUpgrades). - Finish() +func init() { + Table.RegisterFS(rawUpgrades) +} diff --git a/pkg/msgconv/from-matrix.go b/pkg/msgconv/from-matrix.go index 57f9113..49aedf8 100644 --- a/pkg/msgconv/from-matrix.go +++ b/pkg/msgconv/from-matrix.go @@ -150,7 +150,7 @@ func (mc *MessageConverter) ToWhatsApp( } lid := parsedID.Sender if lid.Server == types.DefaultUserServer { - lid, err = client.Store.LIDs.GetLIDForPN(ctx, lid) + lid, err = client.Store.LIDs.GetLIDForPN(ctx, parsedID.Sender) if err != nil { return nil, nil, fmt.Errorf("failed to get LID for PN: %w", err) } diff --git a/pkg/msgconv/from-whatsapp.go b/pkg/msgconv/from-whatsapp.go index 6dac5a2..e8cb032 100644 --- a/pkg/msgconv/from-whatsapp.go +++ b/pkg/msgconv/from-whatsapp.go @@ -136,6 +136,7 @@ func (mc *MessageConverter) ToMatrix( waMsg *waE2E.Message, rawWaMsg *waE2E.Message, info *types.MessageInfo, + origSource *types.MessageSource, isViewOnce bool, isBackfill bool, previouslyConvertedPart *bridgev2.ConvertedMessagePart, @@ -182,7 +183,7 @@ func (mc *MessageConverter) ToMatrix( case waMsg.PollCreationMessageV6 != nil: part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV6) case waMsg.PollUpdateMessage != nil: - part, contextInfo = mc.convertPollUpdateMessage(ctx, info, waMsg.PollUpdateMessage) + part, contextInfo = mc.convertPollUpdateMessage(ctx, info, origSource, waMsg.PollUpdateMessage) case waMsg.EventMessage != nil: part, contextInfo = mc.convertEventMessage(ctx, waMsg.EventMessage) case waMsg.PinInChatMessage != nil: @@ -190,7 +191,7 @@ func (mc *MessageConverter) ToMatrix( case waMsg.KeepInChatMessage != nil: part, contextInfo = mc.convertKeepInChatMessage(ctx, waMsg.KeepInChatMessage) case waMsg.RichResponseMessage != nil: - part, contextInfo = mc.convertRichResponseMessage(ctx, waMsg.RichResponseMessage, waMsg) + part, contextInfo = mc.convertRichResponseMessage(ctx, waMsg.RichResponseMessage) case waMsg.ImageMessage != nil: part, contextInfo = mc.convertMediaMessage(ctx, waMsg.ImageMessage, "photo", info, isViewOnce, previouslyConvertedPart) case waMsg.StickerMessage != nil: @@ -271,26 +272,30 @@ func (mc *MessageConverter) ToMatrix( if chat.IsEmpty() { chat, _ = waid.ParsePortalID(portal.ID) } + // We reroute all DMs to the phone number JID, so reroute reply participants too + pcp = rerouteMessageKey(ctx, chat, pcp, getPortal(ctx).Metadata.(*waid.PortalMetadata).AddressingMode == types.AddressingModeLID) + if store := getClient(ctx).Store; store != nil && chat.Server == types.DefaultUserServer && pcp.Server == types.HiddenUserServer { + pcpPN, _ := store.LIDs.GetPNForLID(ctx, pcp) + zerolog.Ctx(ctx).Debug(). + Stringer("orig_participant", pcp). + Stringer("rerouted_participant", pcpPN). + Msg("Rerouting reply target (PN recipient in LID DM)") + if !pcpPN.IsEmpty() { + pcp = pcpPN + } + } else if store != nil && chat.Server == types.GroupServer && pcp.Server == types.DefaultUserServer && getPortal(ctx).Metadata.(*waid.PortalMetadata).AddressingMode == types.AddressingModeLID { + pcpLID, _ := store.LIDs.GetLIDForPN(ctx, pcp) + zerolog.Ctx(ctx).Debug(). + Stringer("orig_participant", pcp). + Stringer("rerouted_participant", pcpLID). + Msg("Rerouting reply target (PN recipient in LID group)") + if !pcpLID.IsEmpty() { + pcp = pcpLID + } + } cm.ReplyTo = &networkid.MessageOptionalPartID{ MessageID: waid.MakeMessageID(chat, pcp, contextInfo.GetStanzaID()), } - var pn, lid types.JID - if pcp.Server == types.DefaultUserServer { - pn = pcp - lid, _ = client.Store.LIDs.GetLIDForPN(ctx, pcp) - } else if pcp.Server == types.HiddenUserServer { - lid = pcp - pn, _ = client.Store.LIDs.GetPNForLID(ctx, pcp) - } else if pcp.Server == types.BotServer { - lid = pcp - } - if !pn.IsEmpty() { - cm.ReplyToLogin = waid.MakeUserLoginID(pn) - } - if !lid.IsEmpty() { - cm.ReplyToUser = waid.MakeUserID(lid) - } - // TODO set reply to room } if contextInfo.GetIsForwarded() { hasCaption := part.Content.FileName != "" && part.Content.FileName != part.Content.Body diff --git a/pkg/msgconv/wa-misc.go b/pkg/msgconv/wa-misc.go index be5b60f..fa495e0 100644 --- a/pkg/msgconv/wa-misc.go +++ b/pkg/msgconv/wa-misc.go @@ -351,7 +351,7 @@ func (mc *MessageConverter) convertKeepInChatMessage(ctx context.Context, msg *w }, nil } -func (mc *MessageConverter) convertRichResponseMessage(ctx context.Context, msg *waE2E.AIRichResponseMessage, fullMsg *waE2E.Message) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { +func (mc *MessageConverter) convertRichResponseMessage(ctx context.Context, msg *waE2E.AIRichResponseMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { var body strings.Builder // TODO switch to new format? @@ -363,14 +363,10 @@ func (mc *MessageConverter) convertRichResponseMessage(ctx context.Context, msg body.WriteString(submsg.GetMessageText()) } } - unknownPart, _ := mc.convertUnknownMessage(ctx, fullMsg) - if body.Len() == 0 { - return unknownPart, msg.GetContextInfo() - } + content := format.RenderMarkdown(body.String(), true, false) return &bridgev2.ConvertedMessagePart{ Type: event.EventMessage, Content: &content, - Extra: unknownPart.Extra, }, msg.GetContextInfo() } diff --git a/pkg/msgconv/wa-poll.go b/pkg/msgconv/wa-poll.go index 881a5fb..dfe1a25 100644 --- a/pkg/msgconv/wa-poll.go +++ b/pkg/msgconv/wa-poll.go @@ -94,7 +94,31 @@ func (mc *MessageConverter) convertPollCreationMessage(ctx context.Context, msg }, msg.GetContextInfo() } +func rerouteMessageKey(ctx context.Context, chat, sender types.JID, groupLIDAddressing bool) types.JID { + if store := getClient(ctx).Store; store != nil && chat.Server == types.DefaultUserServer && sender.Server == types.HiddenUserServer { + senderPN, _ := store.LIDs.GetPNForLID(ctx, sender) + zerolog.Ctx(ctx).Debug(). + Stringer("orig_participant", sender). + Stringer("rerouted_participant", senderPN). + Msg("Rerouting message key (PN recipient in LID DM)") + if !senderPN.IsEmpty() { + return senderPN + } + } else if store != nil && chat.Server == types.GroupServer && sender.Server == types.DefaultUserServer && groupLIDAddressing { + senderLID, _ := store.LIDs.GetLIDForPN(ctx, sender) + zerolog.Ctx(ctx).Debug(). + Stringer("orig_participant", sender). + Stringer("rerouted_participant", senderLID). + Msg("Rerouting message key (PN recipient in LID group)") + if !senderLID.IsEmpty() { + return senderLID + } + } + return sender +} + func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender types.JID, key *waCommon.MessageKey) networkid.MessageID { + groupLIDAddressing := sender.Server == types.HiddenUserServer sender = sender.ToNonAD() var err error if !key.GetFromMe() { @@ -107,7 +131,7 @@ func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender if sender.Server == types.LegacyUserServer { sender.Server = types.DefaultUserServer } - } else if chat.Server == types.DefaultUserServer || chat.Server == types.HiddenUserServer || chat.Server == types.BotServer { + } else if chat.Server == types.DefaultUserServer || chat.Server == types.BotServer { if sender.User == client.Store.GetJID().User || sender.User == client.Store.GetLID().User { // Message key is not from the sender, but message sender (containing key) is me, // so message key sender is the other user in the DM @@ -115,11 +139,7 @@ func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender } else { // Message key is not from the sender, but message sender (containing key) is not me, // so message key sender is me - if chat.Server == types.HiddenUserServer { - sender = client.Store.GetLID().ToNonAD() - } else { - sender = client.Store.GetJID().ToNonAD() - } + sender = client.Store.GetJID().ToNonAD() } } else { zerolog.Ctx(ctx).Warn(). @@ -137,6 +157,10 @@ func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender chat = remoteJID } } + sender = rerouteMessageKey( + context.WithValue(ctx, contextKeyClient, client), + chat, sender, groupLIDAddressing, + ) return waid.MakeMessageID(chat, sender, key.GetID()) } @@ -146,7 +170,7 @@ var failedPollUpdatePart = &bridgev2.ConvertedMessagePart{ DontBridge: true, } -func (mc *MessageConverter) convertPollUpdateMessage(ctx context.Context, info *types.MessageInfo, msg *waE2E.PollUpdateMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { +func (mc *MessageConverter) convertPollUpdateMessage(ctx context.Context, info *types.MessageInfo, origSource *types.MessageSource, msg *waE2E.PollUpdateMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { log := zerolog.Ctx(ctx) pollMessageID := KeyToMessageID(ctx, getClient(ctx), info.Chat, info.Sender, msg.PollCreationMessageKey) pollMessage, err := mc.Bridge.DB.Message.GetPartByID(ctx, getPortal(ctx).Receiver, pollMessageID, "") @@ -157,8 +181,12 @@ func (mc *MessageConverter) convertPollUpdateMessage(ctx context.Context, info * log.Warn().Str("target_message_id", string(pollMessageID)).Msg("Poll update target message not found") return failedPollUpdatePart, nil } + infoForDecrypt := *info + if origSource != nil { + infoForDecrypt.MessageSource = *origSource + } vote, err := getClient(ctx).DecryptPollVote(ctx, &events.Message{ - Info: *info, + Info: infoForDecrypt, Message: &waE2E.Message{PollUpdateMessage: msg}, }) if err != nil { diff --git a/pkg/waid/dbmeta.go b/pkg/waid/dbmeta.go index e2b60f7..1e54426 100644 --- a/pkg/waid/dbmeta.go +++ b/pkg/waid/dbmeta.go @@ -37,12 +37,12 @@ type UserLoginMetadata struct { PushKeys *PushKeys `json:"push_keys,omitempty"` APNSEncPubKey []byte `json:"apns_enc_pubkey,omitempty"` APNSEncPrivKey []byte `json:"apns_enc_privkey,omitempty"` - LoggedInAt jsontime.Unix `json:"logged_in_at,omitzero"` + LoggedInAt jsontime.Unix `json:"logged_in_at,omitempty"` AppStateRecoveryAttempted map[appstate.WAPatchName]time.Time `json:"app_state_recovery_attempted,omitempty"` - HistorySyncPortalsNeedCreating bool `json:"history_sync_portals_need_creating,omitzero"` - ReachoutTimelockUntil time.Time `json:"reachout_timelock_until,omitzero"` + HistorySyncPortalsNeedCreating bool `json:"history_sync_portals_need_creating,omitempty"` + ReachoutTimelockUntil time.Time `json:"reachout_timelock_until,omitempty"` MData json.RawMessage `json:"mdata,omitempty"` } @@ -88,13 +88,13 @@ type GroupInviteMeta struct { } type MessageMetadata struct { - SenderDeviceID uint16 `json:"sender_device_id,omitzero"` + SenderDeviceID uint16 `json:"sender_device_id,omitempty"` Error MessageErrorType `json:"error,omitempty"` BroadcastListJID *types.JID `json:"broadcast_list_jid,omitempty"` GroupInvite *GroupInviteMeta `json:"group_invite,omitempty"` FailedMediaMeta json.RawMessage `json:"media_meta,omitempty"` DirectMediaMeta json.RawMessage `json:"direct_media_meta,omitempty"` - IsMatrixPoll bool `json:"is_matrix_poll,omitzero"` + IsMatrixPoll bool `json:"is_matrix_poll,omitempty"` Edits []types.MessageID `json:"edits,omitempty"` } @@ -122,16 +122,14 @@ type ReactionMetadata struct { } type PortalMetadata struct { - DisappearingTimerSetAt int64 `json:"disappearing_timer_set_at,omitzero"` + DisappearingTimerSetAt int64 `json:"disappearing_timer_set_at,omitempty"` TopicID string `json:"topic_id,omitempty"` - LastSync jsontime.Unix `json:"last_sync,omitzero"` - CommunityAnnouncementGroup bool `json:"is_cag,omitzero"` + LastSync jsontime.Unix `json:"last_sync,omitempty"` + CommunityAnnouncementGroup bool `json:"is_cag,omitempty"` AddressingMode types.AddressingMode `json:"addressing_mode,omitempty"` - LIDMigrationAttempted bool `json:"lid_migration_attempted,omitzero"` + LIDMigrationAttempted bool `json:"lid_migration_attempted,omitempty"` } type GhostMetadata struct { - LastSync jsontime.Unix `json:"last_sync,omitzero"` - - DirectAvatarURL string `json:"direct_avatar_url,omitempty"` + LastSync jsontime.Unix `json:"last_sync,omitempty"` } diff --git a/pkg/waid/id.go b/pkg/waid/id.go index 7f8fe5b..e522f5f 100644 --- a/pkg/waid/id.go +++ b/pkg/waid/id.go @@ -83,13 +83,6 @@ func MakeMessageID(chat, sender types.JID, id types.MessageID) networkid.Message return networkid.MessageID(fmt.Sprintf("%s:%s:%s", chat.ToNonAD().String(), sender.ToNonAD().String(), id)) } -func MakeMessageIDWithAltSender(chat, sender, altSender types.JID, id types.MessageID) networkid.MessageID { - if chat.Server == types.HiddenUserServer && sender.Server == types.DefaultUserServer && altSender.Server == types.HiddenUserServer { - sender = altSender - } - return MakeMessageID(chat, sender, id) -} - func MakeFakeMessageID(chat, sender types.JID, data string) networkid.MessageID { return networkid.MessageID(fmt.Sprintf("fake:%s:%s:%s", chat.ToNonAD().String(), sender.ToNonAD().String(), data)) } diff --git a/pkg/waid/mediaid.go b/pkg/waid/mediaid.go index a0ac2d9..716e187 100644 --- a/pkg/waid/mediaid.go +++ b/pkg/waid/mediaid.go @@ -24,7 +24,6 @@ import ( "strconv" "strings" - "go.mau.fi/util/random" "go.mau.fi/whatsmeow/types" "maunium.net/go/mautrix/bridgev2/networkid" ) @@ -86,16 +85,6 @@ type AvatarMediaInfo struct { Community bool } -const RandomAvatarIDPrefix = "mxwa-lazy-avatar-" - -func MakeRandomAvatarID() string { - return RandomAvatarIDPrefix + random.String(8) -} - -func (ami *AvatarMediaInfo) IsRandom() bool { - return ami != nil && strings.HasPrefix(ami.AvatarID, RandomAvatarIDPrefix) -} - func MakeStickerPackMediaID(packID string, fileHash []byte, receiver networkid.UserLoginID) networkid.MediaID { receiverID := compactJID(ParseUserLoginID(receiver, 0)) mediaID := make([]byte, 0, 4+len(packID)+len(fileHash)+len(receiverID))