Watch
1
0
Fork
You've already forked mautrix-whatsapp
0
mirror of https://github.com/mautrix/whatsapp.git synced 2026-08-22 12:04:55 -04:00

Compare commits

..
Author SHA1 Message Date
Nick Mills-Barrett
9bd940726f
dependencies: bump whatsmeow 2026-07-09 09:48:24 +01:00
34 changed files with 479 additions and 763 deletions

View file

@ -11,14 +11,14 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
go-version: ["1.26", "1.27"] go-version: ["1.25", "1.26"]
name: Lint ${{ matrix.go-version == '1.27' && '(latest)' || '(old)' }} name: Lint ${{ matrix.go-version == '1.26' && '(latest)' || '(old)' }}
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v6
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v7 uses: actions/setup-go@v6
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}
cache: true cache: true

View file

@ -1,14 +1,3 @@
# v26.08
* Switched direct chats to use LIDs instead of phone numbers.
# v26.07
* Updated Docker image to Alpine 3.24.
* Added support for the extra passkey authentication step when logging in.
* Added support for newer versions of poll messages from WhatsApp.
* Fixed poll votes in DMs.
# v26.06 # v26.06
* Added placeholder for group message history share notices. * Added placeholder for group message history share notices.

View file

@ -1,4 +1,4 @@
FROM golang:1-alpine3.24 AS builder FROM golang:1-alpine3.23 AS builder
RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev
@ -6,7 +6,7 @@ COPY . /build
WORKDIR /build WORKDIR /build
RUN ./build.sh RUN ./build.sh
FROM alpine:3.24 FROM alpine:3.23
ENV UID=1337 \ ENV UID=1337 \
GID=1337 GID=1337

View file

@ -1,6 +1,6 @@
ARG DOCKER_HUB="docker.io" ARG DOCKER_HUB="docker.io"
FROM ${DOCKER_HUB}/alpine:3.24 FROM ${DOCKER_HUB}/alpine:3.23
ENV UID=1337 \ ENV UID=1337 \
GID=1337 GID=1337

View file

@ -18,7 +18,7 @@ var m = mxmain.BridgeMain{
Name: "mautrix-whatsapp", Name: "mautrix-whatsapp",
URL: "https://github.com/mautrix/whatsapp", URL: "https://github.com/mautrix/whatsapp",
Description: "A Matrix-WhatsApp puppeting bridge.", Description: "A Matrix-WhatsApp puppeting bridge.",
Version: "26.08", Version: "26.06",
SemCalVer: true, SemCalVer: true,
Connector: &connector.WhatsAppConnector{}, Connector: &connector.WhatsAppConnector{},
} }

View file

@ -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)
}

34
go.mod
View file

@ -1,8 +1,8 @@
module go.mau.fi/mautrix-whatsapp module go.mau.fi/mautrix-whatsapp
go 1.26.0 go 1.25.0
toolchain go1.27.0 toolchain go1.26.4
tool go.mau.fi/util/cmd/maubuild tool go.mau.fi/util/cmd/maubuild
@ -10,15 +10,15 @@ require (
github.com/lib/pq v1.12.3 github.com/lib/pq v1.12.3
github.com/rs/zerolog v1.35.1 github.com/rs/zerolog v1.35.1
github.com/tidwall/gjson v1.19.0 github.com/tidwall/gjson v1.19.0
go.mau.fi/util v0.10.1-0.20260820140024-eb612d936fde go.mau.fi/util v0.9.10
go.mau.fi/webp v0.3.0 go.mau.fi/webp v0.3.0
go.mau.fi/whatsmeow v0.0.0-20260821141805-33cfac511629 go.mau.fi/whatsmeow v0.0.0-20260708165445-a2136cc8d5db
golang.org/x/image v0.45.0 golang.org/x/image v0.42.0
golang.org/x/net v0.58.0 golang.org/x/net v0.56.0
golang.org/x/sync v0.22.0 golang.org/x/sync v0.21.0
google.golang.org/protobuf v1.36.12 google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
maunium.net/go/mautrix v0.30.1-0.20260820142923-ae00f2c62e1e maunium.net/go/mautrix v0.28.2-0.20260702123919-bf2dbb2aa895
) )
require ( require (
@ -31,8 +31,8 @@ require (
github.com/kr/pretty v0.3.1 // indirect github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.49 // indirect github.com/mattn/go-sqlite3 v1.14.45 // indirect
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b // indirect github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/rs/xid v1.6.0 // indirect github.com/rs/xid v1.6.0 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // 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/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect github.com/tidwall/sjson v1.2.5 // indirect
github.com/vektah/gqlparser/v2 v2.5.27 // indirect github.com/vektah/gqlparser/v2 v2.5.27 // indirect
github.com/yuin/goldmark v1.8.5 // indirect github.com/yuin/goldmark v1.8.2 // indirect
go.mau.fi/libsignal v0.2.2 // indirect go.mau.fi/libsignal v0.2.2 // indirect
go.mau.fi/zeroconfig v0.2.0 // indirect go.mau.fi/zeroconfig v0.2.0 // indirect
golang.org/x/crypto v0.55.0 // indirect golang.org/x/crypto v0.53.0 // indirect
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 // indirect golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect
golang.org/x/mod v0.40.0 // indirect golang.org/x/mod v0.37.0 // indirect
golang.org/x/sys v0.47.0 // indirect golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.41.0 // indirect golang.org/x/text v0.38.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
maunium.net/go/mauflag v1.0.0 // indirect maunium.net/go/mauflag v1.0.0 // indirect

60
go.sum
View file

@ -34,10 +34,10 @@ 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-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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.49 h1:B8jBHC3xhxZgxztrgruTuLucebnULQnx4W7cF7SAE9w= github.com/mattn/go-sqlite3 v1.14.45 h1:6KA/spDguL3KV8rnybG7ezSaE4SeMR3KC9VbUoAQaIk=
github.com/mattn/go-sqlite3 v1.14.49/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= github.com/mattn/go-sqlite3 v1.14.45/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b h1:sS7HLzwS+dO+gxATgQfeZDEdUZe2pKAB3nGoUwP5zU0= github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM=
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
@ -67,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/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 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=
github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/libsignal v0.2.2 h1:QV+XdzQkm3x3aSG7FcqfGSZuFXz83pRZPBFaPygHbOU= 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/libsignal v0.2.2/go.mod h1:CRlIQg2J8uYTfDFvNoO8/KcZjs5cey0vbc6oj/bssY0=
go.mau.fi/util v0.10.1-0.20260820140024-eb612d936fde h1:eMHY9dMDkNuDMWhfTbMZHbbsxj7G6mfujjKei1HaFQM= go.mau.fi/util v0.9.10 h1:wzvz5iDHyqDXB8vgisD4d3SzucLXNM3iNY+1O1RoHtg=
go.mau.fi/util v0.10.1-0.20260820140024-eb612d936fde/go.mod h1:z0ZZNt4hq3FZbUKnunexE/QscCx7VkLvQSvtggc/aE8= go.mau.fi/util v0.9.10/go.mod h1:YQOxySn+ZE3qSYqNxvyX7Yi3suA8YK17PS6QqBREW7A=
go.mau.fi/webp v0.3.0 h1:gVHQZtz21Ziwj+CDuklbX9mqpsnDIFKxs/BJyV7iZzA= 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/webp v0.3.0/go.mod h1:rlZFTev+dYxhvk+XNBP/5GcTt4gXmzAB4DU0aGUYIQo=
go.mau.fi/whatsmeow v0.0.0-20260821141805-33cfac511629 h1:99p9fTS1G73a1aifCVaV5MX4hDq0lVUKWGoUNoXyE3A= go.mau.fi/whatsmeow v0.0.0-20260708165445-a2136cc8d5db h1:giO0YkqIJlbKURx7kj4i7zicLKj2ZODMM5X1tZAadH0=
go.mau.fi/whatsmeow v0.0.0-20260821141805-33cfac511629/go.mod h1:aMd13H2xFFGH9cskcvxo4Aae+TmyFN38yw+HvsrpwVg= go.mau.fi/whatsmeow v0.0.0-20260708165445-a2136cc8d5db/go.mod h1:9dmNTYZ/1pHjPw/bz+azBsGjAkcrZbqzMrKcvG5bJ8U=
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU= go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w= go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 h1:YXnL44eJ77R+ji4/ooy8UsXIhz+lbi2Qgdlc8iRN0gY= golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M=
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297/go.mod h1:Mkmymgv+uMpSQ/XxJ/7GpdrdYoqm3u72jEbpCLiJmNk= golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY=
golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0= golang.org/x/image v0.42.0 h1:1gSs6ehNWXLbkHBIPcWztk3D/6aIA/8hauiAYtlodVY=
golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4= golang.org/x/image v0.42.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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 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 h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@ -107,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= 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 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
maunium.net/go/mautrix v0.30.1-0.20260820142923-ae00f2c62e1e h1:ATvERyBIUXJrnL7ezP2xCLLGCNmGClmfzoQq4N3jBcA= maunium.net/go/mautrix v0.28.2-0.20260702123919-bf2dbb2aa895 h1:oe87/mP1vo4LnA6CJDEfyg5ZiAOar2quJNypfO8eS4Y=
maunium.net/go/mautrix v0.30.1-0.20260820142923-ae00f2c62e1e/go.mod h1:Y02sBiAvfEVqK24bwVGCprmLATRZ7prWel3ZpB413e0= maunium.net/go/mautrix v0.28.2-0.20260702123919-bf2dbb2aa895/go.mod h1:mWXQNmOlrq4VTDU9f1HO03BSIswdUIyyY4wUKHqwzzY=

View file

@ -12,7 +12,6 @@ import (
"time" "time"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.mau.fi/util/exmaps"
"go.mau.fi/util/ptr" "go.mau.fi/util/ptr"
"go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/proto/waE2E" "go.mau.fi/whatsmeow/proto/waE2E"
@ -229,32 +228,30 @@ func (wa *WhatsAppClient) handleWAHistorySync(
} else { } else {
totalMessageCount += len(conv.GetMessages()) totalMessageCount += len(conv.GetMessages())
} }
if jid.Server == types.DefaultUserServer { if jid.Server == types.HiddenUserServer {
lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, jid) pn, err := wa.GetStore().LIDs.GetPNForLID(ctx, jid)
if err != nil { if err != nil {
log.Err(err).Stringer("pn", jid).Msg("Failed to get LID for phone number in history sync") log.Err(err).Stringer("lid", jid).Msg("Failed to get PN for LID in history sync")
} else if lid.IsEmpty() { } else if pn.IsEmpty() {
log.Warn().Stringer("pn", jid).Msg("No LID found for phone number in history sync") log.Warn().Stringer("lid", jid).Msg("No PN found for LID in history sync")
} else { } else {
log.Debug(). log.Debug().
Stringer("lid", lid). Stringer("lid", jid).
Stringer("pn", jid). Stringer("pn", pn).
Msg("Rerouting phone number DM to LID in history sync") Msg("Rerouting LID DM to phone number in history sync")
jid = lid jid = pn
} }
} }
log.UpdateContext(func(c zerolog.Context) zerolog.Context { log.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.Stringer("chat_jid", jid) return c.Stringer("chat_jid", jid)
}) })
var firstItemTime, lastItemTime time.Time var minTime, maxTime, firstItemTime, lastItemTime time.Time
var minTimeIndex, maxTimeIndex int
ignoredTypes := 0 ignoredTypes := 0
rawMessages := conv.GetMessages() messages := make([]*wadb.HistorySyncMessageTuple, 0, len(conv.GetMessages()))
messages := make([]*wadb.HistorySyncMessageTuple, 0, len(rawMessages)) for i, rawMsg := range conv.GetMessages() {
allowClamp := conv.GetCommentsCount() == 0
var newerTS uint64
for i, rawMsg := range rawMessages {
// Don't store messages that will just be skipped. // Don't store messages that will just be skipped.
msgEvt, err := wa.Client.ParseWebMessage(jid, rawMsg.GetMessage()) msgEvt, err := wa.Client.ParseWebMessage(jid, rawMsg.GetMessage())
if err != nil { if err != nil {
@ -272,30 +269,20 @@ func (wa *WhatsAppClient) handleWAHistorySync(
firstItemTime = msgEvt.Info.Timestamp firstItemTime = msgEvt.Info.Timestamp
} }
lastItemTime = 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) msgType := getMessageType(msgEvt.Message)
if msgType == "ignore" || strings.HasPrefix(msgType, "unknown_protocol_") { if msgType == "ignore" || strings.HasPrefix(msgType, "unknown_protocol_") {
ignoredTypes++ ignoredTypes++
continue 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) marshaled, err := proto.Marshal(rawMsg)
if err != nil { if err != nil {
log.Warn().Err(err). log.Warn().Err(err).
@ -310,8 +297,13 @@ func (wa *WhatsAppClient) handleWAHistorySync(
log.Debug(). log.Debug().
Int("wrapped_count", len(messages)). Int("wrapped_count", len(messages)).
Int("ignored_msg_type_count", ignoredTypes). 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("first_item_time", firstItemTime).
Time("last_item_time", lastItemTime). Time("last_item_time", lastItemTime).
Bool("highest_time_mismatch", firstItemTime != maxTime).
Dict("metadata", zerolog.Dict(). Dict("metadata", zerolog.Dict().
Uint32("ephemeral_expiration", conv.GetEphemeralExpiration()). Uint32("ephemeral_expiration", conv.GetEphemeralExpiration()).
Int64("ephemeral_setting_timestamp", conv.GetEphemeralSettingTimestamp()). Int64("ephemeral_setting_timestamp", conv.GetEphemeralSettingTimestamp()).
@ -327,7 +319,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
Msg("Collected messages to save from history sync conversation") Msg("Collected messages to save from history sync conversation")
if len(messages) > 0 { 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 err != nil {
if stopOnError { if stopOnError {
return fmt.Errorf("failed to save conversation metadata for %s: %w", jid, err) 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 { if err != nil {
return nil, err 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 markRead bool
var startTime, endTime *time.Time var startTime, endTime *time.Time
var conv *wadb.Conversation var conv *wadb.Conversation
@ -539,7 +528,7 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
Forward: params.Forward, Forward: params.Forward,
}, nil }, nil
} else if hasMore { } else if hasMore {
return wa.fetchMessagesFromPhone(ctx, portalJID, params) return wa.fetchMessagesFromPhone(ctx, params)
} }
return &bridgev2.FetchMessagesResponse{ return &bridgev2.FetchMessagesResponse{
HasMore: false, HasMore: false,
@ -605,7 +594,6 @@ func (wa *WhatsAppClient) convertHistorySyncMessages(
newestTS := messages[0].GetMessageTimestamp() newestTS := messages[0].GetMessageTimestamp()
convertedMessages := make([]*bridgev2.BackfillMessage, 0, len(messages)) convertedMessages := make([]*bridgev2.BackfillMessage, 0, len(messages))
var mediaRequests []*wadb.MediaRequest var mediaRequests []*wadb.MediaRequest
dups := make(exmaps.Set[networkid.MessageID])
for i, msg := range messages { for i, msg := range messages {
evt, err := wa.Client.ParseWebMessage(portalJID, msg) evt, err := wa.Client.ParseWebMessage(portalJID, msg)
if err != nil { if err != nil {
@ -629,18 +617,10 @@ func (wa *WhatsAppClient) convertHistorySyncMessages(
continue 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 isViewOnce := evt.IsViewOnce || evt.IsViewOnceV2 || evt.IsViewOnceV2Extension
converted, mediaReq := wa.convertHistorySyncMessage( converted, mediaReq := wa.convertHistorySyncMessage(
ctx, portal, &evt.Info, evt.Message, evt.RawMessage, isViewOnce, msg.Reactions, 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) convertedMessages = append(convertedMessages, converted)
if mediaReq != nil { if mediaReq != nil {
mediaRequests = append(mediaRequests, mediaReq) mediaRequests = append(mediaRequests, mediaReq)
@ -671,7 +651,7 @@ func (wa *WhatsAppClient) convertHistorySyncMessages(
}, nil }, 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 { if params.AnchorMessage == nil {
return nil, fmt.Errorf("anchor message is required to fetch messages from phone") return nil, fmt.Errorf("anchor message is required to fetch messages from phone")
} }
@ -683,9 +663,9 @@ func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, portalJID
msgID := wa.Client.GenerateMessageID() msgID := wa.Client.GenerateMessageID()
reqData := wa.Client.BuildHistorySyncRequest(&types.MessageInfo{ reqData := wa.Client.BuildHistorySyncRequest(&types.MessageInfo{
MessageSource: types.MessageSource{ MessageSource: types.MessageSource{
Chat: portalJID, Chat: parsed.Chat,
Sender: parsed.Sender, 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, IsGroup: parsed.Chat.Server == types.GroupServer,
}, },
ID: parsed.ID, ID: parsed.ID,
@ -693,7 +673,6 @@ func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, portalJID
}, 50) }, 50)
zerolog.Ctx(ctx).Debug(). zerolog.Ctx(ctx).Debug().
Str("request_msg_id", msgID). Str("request_msg_id", msgID).
Stringer("portal_jid", portalJID).
Any("anchor_msg_parsed", parsed). Any("anchor_msg_parsed", parsed).
Any("request_data", reqData). Any("request_data", reqData).
Msg("Sending history sync request") Msg("Sending history sync request")
@ -722,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") zerolog.Ctx(ctx).Err(err).Str("jid", conv.GetID()).Msg("Failed to parse portal JID")
continue 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)) portal, err := wa.Main.Bridge.GetPortalByKey(ctx, wa.makeWAPortalKey(portalJID))
if err != nil { if err != nil {
zerolog.Ctx(ctx).Err(err).Stringer("portal_jid", portalJID).Msg("Failed to get portal for on-demand history sync") zerolog.Ctx(ctx).Err(err).Stringer("portal_jid", portalJID).Msg("Failed to get portal for on-demand history sync")
@ -784,12 +749,11 @@ func (wa *WhatsAppClient) convertHistorySyncMessage(
} }
// TODO use proper intent // TODO use proper intent
intent := wa.Main.Bridge.Bot intent := wa.Main.Bridge.Bot
msgID := waid.MakeMessageIDWithAltSender(info.Chat, info.Sender, info.SenderAlt, info.ID)
wrapped := &bridgev2.BackfillMessage{ wrapped := &bridgev2.BackfillMessage{
ConvertedMessage: wa.Main.MsgConv.ToMatrix(ctx, portal, wa.Client, intent, msg, rawMsg, info, isViewOnce, true, nil), 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)), Sender: wa.makeEventSender(ctx, info.Sender),
ID: msgID, ID: waid.MakeMessageID(info.Chat, info.Sender, info.ID),
TxnID: networkid.TransactionID(msgID), TxnID: networkid.TransactionID(waid.MakeMessageID(info.Chat, info.Sender, info.ID)),
Timestamp: info.Timestamp, Timestamp: info.Timestamp,
StreamOrder: info.Timestamp.Unix(), StreamOrder: info.Timestamp.Unix(),
Reactions: make([]*bridgev2.BackfillReaction, 0, len(reactions)), Reactions: make([]*bridgev2.BackfillReaction, 0, len(reactions)),
@ -798,10 +762,10 @@ func (wa *WhatsAppClient) convertHistorySyncMessage(
for _, reaction := range reactions { for _, reaction := range reactions {
var sender types.JID var sender types.JID
if reaction.GetKey().GetFromMe() { if reaction.GetKey().GetFromMe() {
sender = wa.GetLID() sender = wa.JID
} else if reaction.GetKey().GetParticipant() != "" { } else if reaction.GetKey().GetParticipant() != "" {
sender, _ = types.ParseJID(*reaction.Key.Participant) 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 sender = info.Chat
} }
if sender.IsEmpty() { if sender.IsEmpty() {

View file

@ -52,7 +52,7 @@ func (wa *WhatsAppConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilit
} }
func (wa *WhatsAppConnector) GetBridgeInfoVersion() (info, caps int) { func (wa *WhatsAppConnector) GetBridgeInfoVersion() (info, caps int) {
return 1, 9 return 1, 8
} }
const WAMaxFileSize = 2000 * 1024 * 1024 const WAMaxFileSize = 2000 * 1024 * 1024
@ -67,7 +67,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel {
} }
func capID() string { func capID() string {
base := "fi.mau.whatsapp.capabilities.2026_07_22" base := "fi.mau.whatsapp.capabilities.2026_05_12"
if ffmpeg.Supported() { if ffmpeg.Supported() {
return base + "+ffmpeg" return base + "+ffmpeg"
} }
@ -177,11 +177,6 @@ var whatsappCaps = &event.RoomFeatures{
MaxTextLength: MaxTextLength, MaxTextLength: MaxTextLength,
LocationMessage: event.CapLevelFullySupported, LocationMessage: event.CapLevelFullySupported,
Poll: event.CapLevelFullySupported, Poll: event.CapLevelFullySupported,
PollEnd: event.CapLevelUnsupported,
PollHiddenVotes: event.CapLevelUnsupported,
PollDuplicateOptions: event.CapLevelUnsupported,
PollMaxOptions: 12,
PollOptionMaxLength: 100,
Reply: event.CapLevelFullySupported, Reply: event.CapLevelFullySupported,
Edit: event.CapLevelFullySupported, Edit: event.CapLevelFullySupported,
EditMaxAge: ptr.Ptr(jsontime.S(EditMaxAge)), EditMaxAge: ptr.Ptr(jsontime.S(EditMaxAge)),

View file

@ -99,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") zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get chat settings")
return 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{ info.UserLocal = &bridgev2.UserLocalPortalInfo{
MutedUntil: ptr.Ptr(chat.MutedUntil), MutedUntil: ptr.Ptr(chat.MutedUntil),
} }
@ -153,12 +141,7 @@ const PrivateChatTopic = "WhatsApp private chat"
const BotChatTopic = "WhatsApp chat with a bot" const BotChatTopic = "WhatsApp chat with a bot"
func (wa *WhatsAppClient) wrapDMInfo(ctx context.Context, jid types.JID) *bridgev2.ChatInfo { 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{ info := &bridgev2.ChatInfo{
Type: ptr.Ptr(database.RoomTypeDM),
Topic: ptr.Ptr(PrivateChatTopic), Topic: ptr.Ptr(PrivateChatTopic),
Members: &bridgev2.ChatMemberList{ Members: &bridgev2.ChatMemberList{
IsFull: true, IsFull: true,
@ -166,7 +149,7 @@ func (wa *WhatsAppClient) wrapDMInfo(ctx context.Context, jid types.JID) *bridge
OtherUserID: waid.MakeUserID(jid), OtherUserID: waid.MakeUserID(jid),
MemberMap: map[networkid.UserID]bridgev2.ChatMember{ MemberMap: map[networkid.UserID]bridgev2.ChatMember{
waid.MakeUserID(jid): {EventSender: wa.makeEventSender(ctx, jid)}, waid.MakeUserID(jid): {EventSender: wa.makeEventSender(ctx, jid)},
waid.MakeUserID(ownID): {EventSender: wa.makeEventSender(ctx, ownID)}, waid.MakeUserID(wa.JID): {EventSender: wa.makeEventSender(ctx, wa.JID)},
}, },
PowerLevels: &bridgev2.PowerLevelOverrides{ PowerLevels: &bridgev2.PowerLevelOverrides{
Events: map[event.Type]int{ Events: map[event.Type]int{
@ -176,14 +159,13 @@ func (wa *WhatsAppClient) wrapDMInfo(ctx context.Context, jid types.JID) *bridge
event.StateBeeperDisappearingTimer: 0, event.StateBeeperDisappearingTimer: 0,
}, },
}, },
ExcludeChangesFromTimeline: true,
}, },
ExcludeChangesFromTimeline: true, Type: ptr.Ptr(database.RoomTypeDM),
} }
if jid.Server == types.BotServer { if jid.Server == types.BotServer {
info.Topic = ptr.Ptr(BotChatTopic) 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. // 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{ info.Members.MemberMap = map[networkid.UserID]bridgev2.ChatMember{
waid.MakeUserID(jid): {EventSender: bridgev2.EventSender{Sender: waid.MakeUserID(jid)}}, waid.MakeUserID(jid): {EventSender: bridgev2.EventSender{Sender: waid.MakeUserID(jid)}},
@ -207,7 +189,7 @@ func (wa *WhatsAppClient) wrapStatusBroadcastInfo(ctx context.Context) *bridgev2
Members: &bridgev2.ChatMemberList{ Members: &bridgev2.ChatMemberList{
IsFull: false, IsFull: false,
MemberMap: map[networkid.UserID]bridgev2.ChatMember{ 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), Type: ptr.Ptr(database.RoomTypeDefault),
@ -289,8 +271,9 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
event.StateTopic: metaChangePL, event.StateTopic: metaChangePL,
event.EventReaction: defaultPL, event.EventReaction: defaultPL,
event.EventRedaction: defaultPL, event.EventRedaction: defaultPL,
event.EventUnstablePollResponse: defaultPL,
event.StateBeeperDisappearingTimer: metaChangePL, event.StateBeeperDisappearingTimer: metaChangePL,
// TODO always allow poll responses
}, },
}, },
}, },
@ -333,7 +316,7 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
} }
} }
if info.IsParent && !hasSelf && info.AddressingMode == types.AddressingModeLID { 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() { if !info.LinkedParentJID.IsEmpty() {
@ -540,8 +523,8 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne
Members: &bridgev2.ChatMemberList{ Members: &bridgev2.ChatMemberList{
TotalMemberCount: info.ThreadMeta.SubscriberCount, TotalMemberCount: info.ThreadMeta.SubscriberCount,
MemberMap: map[networkid.UserID]bridgev2.ChatMember{ MemberMap: map[networkid.UserID]bridgev2.ChatMember{
waid.MakeUserID(wa.GetLID()): { waid.MakeUserID(wa.JID): {
EventSender: wa.makeEventSender(ctx, wa.GetLID()), EventSender: wa.makeEventSender(ctx, wa.JID),
PowerLevel: &ownPowerLevel, PowerLevel: &ownPowerLevel,
}, },
}, },
@ -555,7 +538,7 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne
event.StateTopic: adminPL, event.StateTopic: adminPL,
event.EventReaction: defaultPL, event.EventReaction: defaultPL,
event.EventRedaction: defaultPL, event.EventRedaction: defaultPL,
event.EventUnstablePollResponse: defaultPL, // TODO always allow poll responses
}, },
}, },
}, },

View file

@ -54,9 +54,8 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2.
directMediaRetries: make(map[networkid.MessageID]*directMediaRetry), directMediaRetries: make(map[networkid.MessageID]*directMediaRetry),
mediaRetryLock: semaphore.NewWeighted(wa.Config.HistorySync.MediaRequests.MaxAsyncHandle), mediaRetryLock: semaphore.NewWeighted(wa.Config.HistorySync.MediaRequests.MaxAsyncHandle),
pushNamesSynced: exsync.NewEvent(), pushNamesSynced: exsync.NewEvent(),
createDedup: exsync.NewSet[types.MessageID](),
appStateFullSyncAttempted: make(map[appstate.WAPatchName]time.Time), appStateFullSyncAttempted: make(map[appstate.WAPatchName]time.Time),
disableNewsletter: store.BaseClientPayload.GetUserAgent().GetPlatform() == waWa6.ClientPayload_UserAgent_MACOS,
} }
login.Client = w login.Client = w
@ -71,7 +70,6 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2.
if err != nil { if err != nil {
return err return err
} }
w.LID = w.Device.GetLID()
if w.Device != nil { if w.Device != nil {
log := w.UserLogin.Log.With().Str("component", "whatsmeow").Logger() log := w.UserLogin.Log.With().Str("component", "whatsmeow").Logger()
@ -106,7 +104,6 @@ type WhatsAppClient struct {
Client *whatsmeow.Client Client *whatsmeow.Client
Device *store.Device Device *store.Device
JID types.JID JID types.JID
LID types.JID
MC mClient MC mClient
historySyncWakeup chan struct{} historySyncWakeup chan struct{}
@ -121,8 +118,7 @@ type WhatsAppClient struct {
isNewLogin bool isNewLogin bool
pushNamesSynced *exsync.Event pushNamesSynced *exsync.Event
lastPresence types.Presence lastPresence types.Presence
createDedup *exsync.Set[types.MessageID]
disableNewsletter bool
appStateRecoveryLock sync.Mutex appStateRecoveryLock sync.Mutex
appStateFullSyncAttempted map[appstate.WAPatchName]time.Time appStateFullSyncAttempted map[appstate.WAPatchName]time.Time
@ -189,19 +185,7 @@ func (wa *WhatsAppClient) RegisterPushNotifications(ctx context.Context, pushTyp
} }
func (wa *WhatsAppClient) IsThisUser(_ context.Context, userID networkid.UserID) bool { func (wa *WhatsAppClient) IsThisUser(_ context.Context, userID networkid.UserID) bool {
return userID == waid.MakeUserID(wa.JID) || userID == waid.MakeUserID(wa.GetLID()) return userID == waid.MakeUserID(wa.JID)
}
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
} }
func (wa *WhatsAppClient) Connect(ctx context.Context) { func (wa *WhatsAppClient) Connect(ctx context.Context) {

View file

@ -28,24 +28,27 @@ import (
"time" "time"
"github.com/lib/pq" "github.com/lib/pq"
"github.com/rs/zerolog"
"go.mau.fi/util/dbutil" "go.mau.fi/util/dbutil"
"go.mau.fi/util/exsync"
"go.mau.fi/util/random" "go.mau.fi/util/random"
"go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/proto/waCompanionReg" "go.mau.fi/whatsmeow/proto/waCompanionReg"
"go.mau.fi/whatsmeow/store" "go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/store/sqlstore" "go.mau.fi/whatsmeow/store/sqlstore"
whatsmeowUpgrades "go.mau.fi/whatsmeow/store/sqlstore/upgrades" whatsmeowUpgrades "go.mau.fi/whatsmeow/store/sqlstore/upgrades"
"go.mau.fi/whatsmeow/types"
waLog "go.mau.fi/whatsmeow/util/log" waLog "go.mau.fi/whatsmeow/util/log"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/commands" "maunium.net/go/mautrix/bridgev2/commands"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-whatsapp/pkg/connector/wadb" "go.mau.fi/mautrix-whatsapp/pkg/connector/wadb"
"go.mau.fi/mautrix-whatsapp/pkg/msgconv" "go.mau.fi/mautrix-whatsapp/pkg/msgconv"
"go.mau.fi/mautrix-whatsapp/pkg/waid"
) )
type WhatsAppConnector struct { type WhatsAppConnector struct {
@ -61,8 +64,6 @@ type WhatsAppConnector struct {
mediaEditCache MediaEditCache mediaEditCache MediaEditCache
mediaEditCacheLock sync.RWMutex mediaEditCacheLock sync.RWMutex
stopMediaEditCacheLoop atomic.Pointer[context.CancelFunc] stopMediaEditCacheLoop atomic.Pointer[context.CancelFunc]
unmigratedDMs *exsync.Set[networkid.PortalKey]
} }
func init() { func init() {
@ -113,7 +114,6 @@ func (wa *WhatsAppConnector) Init(bridge *bridgev2.Bridge) {
cmdAccept, cmdSync, cmdInviteLink, cmdResolveLink, cmdJoin, cmdAccept, cmdSync, cmdInviteLink, cmdResolveLink, cmdJoin,
) )
wa.mediaEditCache = make(MediaEditCache) wa.mediaEditCache = make(MediaEditCache)
wa.unmigratedDMs = exsync.NewSet[networkid.PortalKey]()
whatsmeowDBLog := bridge.Log.With().Str("db_section", "whatsmeow").Logger() whatsmeowDBLog := bridge.Log.With().Str("db_section", "whatsmeow").Logger()
wa.DeviceStore = sqlstore.NewWithWrappedDB( wa.DeviceStore = sqlstore.NewWithWrappedDB(
@ -157,7 +157,78 @@ func (wa *WhatsAppConnector) Start(ctx context.Context) error {
return bridgev2.DBUpgradeError{Err: err, Section: "whatsapp"} return bridgev2.DBUpgradeError{Err: err, Section: "whatsapp"}
} }
return wa.migrateToLIDDMs(ctx) if !wa.Bridge.Background && wa.Bridge.DB.KV.Get(ctx, "whatsapp_lid_dms_deleted") == "false" {
wa.deleteLIDDMsMigration(ctx)
}
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() { func (wa *WhatsAppConnector) Stop() {

View file

@ -52,7 +52,7 @@ func (wa *WhatsAppConnector) SetUseDirectMedia() {
} }
var ErrReloadNeeded = mautrix.RespError{ 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", Err: "Media is no longer available on WhatsApp servers and must be re-requested from your phone",
StatusCode: http.StatusNotFound, StatusCode: http.StatusNotFound,
} }
@ -212,7 +212,7 @@ func (wa *WhatsAppConnector) makeDirectMediaResponse(
log := zerolog.Ctx(ctx) log := zerolog.Ctx(ctx)
err := waClient.Client.DownloadToFile(ctx, dm, f) 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)) { 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") { if val == "false" || (!wa.Config.DirectMediaAutoRequest && val != "true") {
return nil, ErrReloadNeeded return nil, ErrReloadNeeded
} }

View file

@ -39,6 +39,18 @@ import (
"go.mau.fi/mautrix-whatsapp/pkg/waid" "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 { type MessageInfoWrapper struct {
Info types.MessageInfo Info types.MessageInfo
wa *WhatsAppClient wa *WhatsAppClient
@ -49,26 +61,7 @@ func (evt *MessageInfoWrapper) ShouldCreatePortal() bool {
} }
func (evt *MessageInfoWrapper) GetPortalKey() networkid.PortalKey { func (evt *MessageInfoWrapper) GetPortalKey() networkid.PortalKey {
ms := evt.Info.MessageSource return evt.wa.getPortalKeyByMessageSource(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)
} }
func (evt *MessageInfoWrapper) AddLogContext(c zerolog.Context) zerolog.Context { func (evt *MessageInfoWrapper) AddLogContext(c zerolog.Context) zerolog.Context {
@ -79,19 +72,12 @@ func (evt *MessageInfoWrapper) GetTimestamp() time.Time {
return evt.Info.Timestamp 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 { 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 { 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 { func (evt *MessageInfoWrapper) GetTransactionID() networkid.TransactionID {
@ -148,6 +134,14 @@ func (evt *WAMessageEvent) PreHandle(ctx context.Context, portal *bridgev2.Porta
return return
} }
meta := portal.Metadata.(*waid.PortalMetadata) 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 { if meta.AddressingMode == types.AddressingModeLID || meta.LIDMigrationAttempted {
return return
} }
@ -166,6 +160,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") 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{}) portal.UpdateInfo(ctx, evt.wa.wrapGroupInfo(ctx, info), evt.wa.UserLogin, nil, time.Time{})
log.Debug().Msg("Finished resyncing after LID change") 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) { func (evt *WAMessageEvent) PostHandle(ctx context.Context, portal *bridgev2.Portal) {
@ -391,8 +392,6 @@ func (evt *WAUndecryptableMessage) GetStreamOrder() int64 {
type WAMediaRetry struct { type WAMediaRetry struct {
*events.MediaRetry *events.MediaRetry
wa *WhatsAppClient wa *WhatsAppClient
senderLID types.JID
chatLID types.JID
} }
func (evt *WAMediaRetry) GetType() bridgev2.RemoteEventType { func (evt *WAMediaRetry) GetType() bridgev2.RemoteEventType {
@ -400,7 +399,7 @@ func (evt *WAMediaRetry) GetType() bridgev2.RemoteEventType {
} }
func (evt *WAMediaRetry) GetPortalKey() networkid.PortalKey { 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 { func (evt *WAMediaRetry) AddLogContext(c zerolog.Context) zerolog.Context {
@ -414,23 +413,16 @@ func (evt *WAMediaRetry) AddLogContext(c zerolog.Context) zerolog.Context {
func (evt *WAMediaRetry) getRealSender() types.JID { func (evt *WAMediaRetry) getRealSender() types.JID {
sender := evt.SenderID sender := evt.SenderID
if sender.IsEmpty() {
if evt.FromMe { if evt.FromMe {
if evt.ChatID.Server == types.HiddenUserServer {
sender = evt.wa.GetLID().ToNonAD()
} else {
sender = evt.wa.JID.ToNonAD() sender = evt.wa.JID.ToNonAD()
} } else if sender.IsEmpty() && (evt.ChatID.Server == types.DefaultUserServer || evt.ChatID.Server == types.BotServer) {
} else if evt.ChatID.Server == types.DefaultUserServer || evt.ChatID.Server == types.HiddenUserServer || evt.ChatID.Server == types.BotServer {
sender = evt.ChatID.ToNonAD() sender = evt.ChatID.ToNonAD()
} }
}
return sender return sender
} }
func (evt *WAMediaRetry) GetSender() bridgev2.EventSender { func (evt *WAMediaRetry) GetSender() bridgev2.EventSender {
realSender := pickLID(evt.getRealSender(), evt.senderLID) return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, evt.getRealSender())
return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, realSender)
} }
func (evt *WAMediaRetry) GetTargetMessage() networkid.MessageID { func (evt *WAMediaRetry) GetTargetMessage() networkid.MessageID {

View file

@ -103,14 +103,11 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg
if err != nil { if err != nil {
return nil, err 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 { if chatJID == types.StatusBroadcastJID && wa.Main.Config.DisableStatusBroadcastSend {
return nil, ErrBroadcastSendDisabled return nil, ErrBroadcastSendDisabled
} }
wrappedMsgID := waid.MakeMessageID(chatJID, wa.JID, req.ID) 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(wrappedMsgID))
msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID2)) msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID2))
zerolog.Ctx(ctx).Trace().Any("payload", waMsg).Msg("Outgoing message payload") 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 return nil, err
} }
var pickedMessageID networkid.MessageID var pickedMessageID networkid.MessageID
if resp.Sender == wa.GetLID() { if resp.Sender == wa.GetStore().GetLID() && chatJID.Server != types.DefaultUserServer {
pickedMessageID = wrappedMsgID2 pickedMessageID = wrappedMsgID2
msg.RemovePending(networkid.TransactionID(wrappedMsgID)) msg.RemovePending(networkid.TransactionID(wrappedMsgID))
} else { } else {
@ -140,17 +137,18 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg
}, nil }, 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) portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil { if err != nil {
return bridgev2.MatrixReactionPreResponse{}, fmt.Errorf("failed to parse portal ID: %w", err) return bridgev2.MatrixReactionPreResponse{}, fmt.Errorf("failed to parse portal ID: %w", err)
} else if portalJID == types.StatusBroadcastJID { } else if portalJID == types.StatusBroadcastJID {
return bridgev2.MatrixReactionPreResponse{}, ErrBroadcastReactionUnsupported return bridgev2.MatrixReactionPreResponse{}, ErrBroadcastReactionUnsupported
} }
sender := wa.GetLID() sender := wa.JID
if portalJID.Server == types.DefaultUserServer { if portalJID.Server == types.HiddenUserServer ||
zerolog.Ctx(ctx).Warn().Stringer("portal_jid", portalJID).Msg("Matrix reaction received in phone number portal") msg.Portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup ||
sender = wa.JID msg.Portal.Metadata.(*waid.PortalMetadata).AddressingMode == types.AddressingModeLID {
sender = wa.GetStore().GetLID()
} }
return bridgev2.MatrixReactionPreResponse{ return bridgev2.MatrixReactionPreResponse{
SenderID: waid.MakeUserID(sender), SenderID: waid.MakeUserID(sender),
@ -179,7 +177,7 @@ func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev
defer wa.mcTrack(msg, time.Now(), &retErr) defer wa.mcTrack(msg, time.Now(), &retErr)
var req whatsmeow.SendRequestExtra var req whatsmeow.SendRequestExtra
if msg.Portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup { if msg.Portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup {
reactionMsg.EncReactionMessage, err = wa.Client.EncryptReaction(ctx, msgconv.MessageIDToInfo(ctx, wa.Client, messageID), reactionMsg.ReactionMessage) reactionMsg.EncReactionMessage, err = wa.Client.EncryptReaction(ctx, msgconv.MessageIDToInfo(wa.Client, messageID), reactionMsg.ReactionMessage)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to encrypt reaction: %w", err) return nil, fmt.Errorf("failed to encrypt reaction: %w", err)
} }
@ -322,7 +320,7 @@ func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *
if err != nil { if err != nil {
continue continue
} }
if wa.IsOwnJID(parsed.Sender) { if parsed.Sender.User == wa.GetStore().GetLID().User || parsed.Sender.User == wa.JID.User {
continue continue
} }
var key types.JID var key types.JID
@ -638,7 +636,7 @@ func (wa *WhatsAppClient) getLastMessageInfo(ctx context.Context, chatJID types.
lastTS = msgs[0].Timestamp lastTS = msgs[0].Timestamp
parsed, _ := waid.ParseMessageID(msgs[0].ID) parsed, _ := waid.ParseMessageID(msgs[0].ID)
if parsed != nil { 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 var participant *string
if chatJID.Server == types.GroupServer { if chatJID.Server == types.GroupServer {
participant = ptr.Ptr(parsed.Sender.String()) participant = ptr.Ptr(parsed.Sender.String())

View file

@ -113,7 +113,8 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
case *events.HistorySync: case *events.HistorySync:
wa.UserLogin.Log.Warn().Msg("Unexpected history sync event received") wa.UserLogin.Log.Warn().Msg("Unexpected history sync event received")
case *events.MediaRetry: 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: case *events.GroupInfo:
success = wa.handleWAGroupInfoChange(ctx, evt) success = wa.handleWAGroupInfoChange(ctx, evt)
@ -157,7 +158,7 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
if err != nil { if err != nil {
log.Err(err).Msg("Failed to update push name in store") 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 { if err != nil {
log.Err(err).Msg("Failed to update push name in store") log.Err(err).Msg("Failed to update push name in store")
} }
@ -258,26 +259,65 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
return return
} }
func (wa *WhatsAppClient) ensureAltJIDs(ctx context.Context, info *types.MessageSource, checkPhones bool) bool { func (wa *WhatsAppClient) rerouteWAMessage(ctx context.Context, evtType string, info *types.MessageSource, msgID any) {
var err error if (info.Chat.Server == types.HiddenUserServer || info.Chat.Server == types.BroadcastServer) &&
if info.Sender.Server == types.DefaultUserServer && info.SenderAlt.IsEmpty() { info.Sender.Server == types.HiddenUserServer && info.SenderAlt.IsEmpty() {
info.SenderAlt, err = wa.GetStore().LIDs.GetLIDForPN(ctx, info.Sender) 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 { if err != nil {
zerolog.Ctx(ctx).Err(err).Stringer("sender", info.Sender).Msg("Failed to get LID for sender") wa.UserLogin.Log.Err(err).
return false 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) { func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Message) (success bool) {
@ -285,12 +325,6 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
if evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast { if evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast {
return return
} }
if evt.Info.Chat.Server == types.NewsletterServer && wa.disableNewsletter {
return
}
if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, true) {
return false
}
parsedMessageType := getMessageType(evt.Message) parsedMessageType := getMessageType(evt.Message)
if encReact := evt.Message.GetEncReactionMessage(); encReact != nil { if encReact := evt.Message.GetEncReactionMessage(); encReact != nil {
decrypted, err := wa.Client.DecryptReaction(ctx, evt) decrypted, err := wa.Client.DecryptReaction(ctx, evt)
@ -324,6 +358,7 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
evt.UnwrapRaw() evt.UnwrapRaw()
parsedMessageType = getMessageType(evt.Message) parsedMessageType = getMessageType(evt.Message)
} }
wa.rerouteWAMessage(ctx, "message", &evt.Info.MessageSource, evt.Info.ID)
wa.UserLogin.Log.Trace(). wa.UserLogin.Log.Trace().
Any("info", evt.Info). Any("info", evt.Info).
Any("payload", evt.Message). Any("payload", evt.Message).
@ -387,9 +422,7 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
} }
func (wa *WhatsAppClient) handleWAUndecryptableMessage(ctx context.Context, evt *events.UndecryptableMessage) bool { func (wa *WhatsAppClient) handleWAUndecryptableMessage(ctx context.Context, evt *events.UndecryptableMessage) bool {
if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, true) { wa.rerouteWAMessage(ctx, "undecryptable message", &evt.Info.MessageSource, evt.Info.ID)
return false
}
wa.UserLogin.Log.Debug(). wa.UserLogin.Log.Debug().
Any("info", evt.Info). Any("info", evt.Info).
Bool("unavailable", evt.IsUnavailable). Bool("unavailable", evt.IsUnavailable).
@ -412,44 +445,12 @@ func (wa *WhatsAppClient) handleWAUndecryptableMessage(ctx context.Context, evt
return res.Success 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) { 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 { if evt.IsFromMe && evt.Sender.Device == 0 {
wa.phoneSeen(evt.Timestamp) wa.phoneSeen(evt.Timestamp)
} }
if !wa.ensureAltJIDs(ctx, &evt.MessageSource, true) {
return false
}
var evtType bridgev2.RemoteEventType var evtType bridgev2.RemoteEventType
switch evt.Type { switch evt.Type {
case types.ReceiptTypeRead, types.ReceiptTypeReadSelf: case types.ReceiptTypeRead, types.ReceiptTypeReadSelf:
@ -461,34 +462,28 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei
default: default:
return true return true
} }
targets := make([]networkid.MessageID, 0, len(evt.MessageIDs)) targets := make([]networkid.MessageID, len(evt.MessageIDs))
messageSender := wa.GetLID() messageSender := wa.JID
if !evt.MessageSender.IsEmpty() { if !evt.MessageSender.IsEmpty() {
messageSender = evt.MessageSender messageSender = evt.MessageSender
// Second part of rerouting receipts in LID chats
if messageSender == origChat && evt.Chat != origChat {
messageSender = evt.Chat
} }
var chatAlt types.JID } else if evt.Chat.Server == types.GroupServer && evt.Sender.Server == types.HiddenUserServer {
if evt.Chat.Server == types.DefaultUserServer { lid := wa.GetStore().GetLID()
chatLID, _ := wa.GetStore().LIDs.GetLIDForPN(ctx, evt.Chat) if !lid.IsEmpty() {
if !chatLID.IsEmpty() { messageSender = lid
chatAlt = evt.Chat
evt.Chat = chatLID
} }
} }
for _, id := range evt.MessageIDs { for i, id := range evt.MessageIDs {
targets = append(targets, waid.MakeMessageID(evt.Chat, messageSender, id)) targets[i] = 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
} }
res := wa.UserLogin.QueueRemoteEvent(&simplevent.Receipt{ res := wa.UserLogin.QueueRemoteEvent(&simplevent.Receipt{
EventMeta: simplevent.EventMeta{ EventMeta: simplevent.EventMeta{
Type: evtType, Type: evtType,
PortalKey: wa.makeWAPortalKey(evt.Chat), PortalKey: wa.makeWAPortalKey(evt.Chat),
Sender: wa.makeEventSender(ctx, senderLID), Sender: wa.makeEventSender(ctx, evt.Sender),
Timestamp: evt.Timestamp, Timestamp: evt.Timestamp,
}, },
Targets: targets, Targets: targets,
@ -497,11 +492,11 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei
} }
func (wa *WhatsAppClient) handleWAChatPresence(ctx context.Context, evt *events.ChatPresence) { 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() { 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.Sender, evt.SenderAlt = evt.SenderAlt, evt.Sender
evt.Chat = evt.Sender.ToNonAD() evt.Chat = evt.Sender.ToNonAD()
} }
@ -538,7 +533,6 @@ func (wa *WhatsAppClient) handleWALogout(reason events.ConnectFailureReason, onC
wa.Disconnect() wa.Disconnect()
wa.Client = nil wa.Client = nil
wa.JID = types.EmptyJID wa.JID = types.EmptyJID
wa.LID = types.EmptyJID
wa.UserLogin.Metadata.(*waid.UserLoginMetadata).WADeviceID = 0 wa.UserLogin.Metadata.(*waid.UserLoginMetadata).WADeviceID = 0
wa.UserLogin.BridgeState.Send(status.BridgeState{ wa.UserLogin.BridgeState.Send(status.BridgeState{
StateEvent: status.StateBadCredentials, StateEvent: status.StateBadCredentials,
@ -552,15 +546,12 @@ func (wa *WhatsAppClient) handleWACallStart(ctx context.Context, group, sender,
if !wa.Main.Config.CallStartNotices || time.Since(ts) > callEventMaxAge { if !wa.Main.Config.CallStartNotices || time.Since(ts) > callEventMaxAge {
return true return true
} }
if sender.Server == types.DefaultUserServer && senderAlt.IsEmpty() { if sender.Server == types.HiddenUserServer && senderAlt.Server == types.DefaultUserServer {
senderAlt, _ = wa.GetStore().LIDs.GetLIDForPN(ctx, sender)
}
if sender.Server == types.DefaultUserServer && senderAlt.Server == types.HiddenUserServer {
wa.UserLogin.Log.Debug(). wa.UserLogin.Log.Debug().
Stringer("lid", senderAlt). Stringer("lid", sender).
Stringer("pn", sender). Stringer("pn", senderAlt).
Str("call_id", id). 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 sender, senderAlt = senderAlt, sender
} }
chat := group chat := group
@ -607,12 +598,6 @@ func (wa *WhatsAppClient) handleWAIdentityChange(ctx context.Context, evt *event
if !wa.Main.Config.IdentityChangeNotices { if !wa.Main.Config.IdentityChangeNotices {
return 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]{ wa.UserLogin.QueueRemoteEvent(&simplevent.Message[*events.IdentityChange]{
EventMeta: simplevent.EventMeta{ EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventMessage, Type: bridgev2.RemoteEventMessage,
@ -663,14 +648,13 @@ func (wa *WhatsAppClient) handleWADeleteChat(ctx context.Context, evt *events.De
func (wa *WhatsAppClient) handleWADeleteForMe(ctx context.Context, evt *events.DeleteForMe) bool { func (wa *WhatsAppClient) handleWADeleteForMe(ctx context.Context, evt *events.DeleteForMe) bool {
chatJID := wa.maybeConvertJIDToLID(ctx, evt.ChatJID) chatJID := wa.maybeConvertJIDToLID(ctx, evt.ChatJID)
senderJID := wa.maybeConvertJIDToLID(ctx, evt.SenderJID)
return wa.UserLogin.QueueRemoteEvent(&simplevent.MessageRemove{ return wa.UserLogin.QueueRemoteEvent(&simplevent.MessageRemove{
EventMeta: simplevent.EventMeta{ EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventMessageRemove, Type: bridgev2.RemoteEventMessageRemove,
PortalKey: wa.makeWAPortalKey(chatJID), PortalKey: wa.makeWAPortalKey(chatJID),
Timestamp: evt.Timestamp, Timestamp: evt.Timestamp,
}, },
TargetMessage: waid.MakeMessageID(chatJID, senderJID, evt.MessageID), TargetMessage: waid.MakeMessageID(chatJID, evt.SenderJID, evt.MessageID),
OnlyForMe: true, OnlyForMe: true,
}).Success }).Success
} }
@ -681,7 +665,7 @@ func (wa *WhatsAppClient) handleWAMarkChatAsRead(ctx context.Context, evt *event
EventMeta: simplevent.EventMeta{ EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventReadReceipt, Type: bridgev2.RemoteEventReadReceipt,
PortalKey: wa.makeWAPortalKey(chatJID), PortalKey: wa.makeWAPortalKey(chatJID),
Sender: wa.makeEventSender(ctx, wa.GetLID()), Sender: wa.makeEventSender(ctx, wa.JID),
Timestamp: evt.Timestamp, Timestamp: evt.Timestamp,
}, },
ReadUpTo: evt.Timestamp, ReadUpTo: evt.Timestamp,
@ -770,6 +754,9 @@ func (wa *WhatsAppClient) handleWAGroupInfoChange(ctx context.Context, evt *even
} }
func (wa *WhatsAppClient) handleWAJoinedGroup(ctx context.Context, evt *events.JoinedGroup) bool { 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{ return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{
EventMeta: simplevent.EventMeta{ EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventChatResync, Type: bridgev2.RemoteEventChatResync,
@ -782,9 +769,6 @@ func (wa *WhatsAppClient) handleWAJoinedGroup(ctx context.Context, evt *events.J
} }
func (wa *WhatsAppClient) handleWANewsletterJoin(ctx context.Context, evt *events.NewsletterJoin) bool { func (wa *WhatsAppClient) handleWANewsletterJoin(ctx context.Context, evt *events.NewsletterJoin) bool {
if wa.disableNewsletter {
return true
}
return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{ return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{
EventMeta: simplevent.EventMeta{ EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventChatResync, Type: bridgev2.RemoteEventChatResync,
@ -883,12 +867,7 @@ func (wa *WhatsAppClient) handleWAAppStateSyncComplete(ctx context.Context, evt
} else { } else {
log.Info(). log.Info().
Time("recovery_ts", ts). Time("recovery_ts", ts).
Bool("recovery_evt", evt.Recovery).
Msg("Unmarked app state recovery as attempted after successful full sync") 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 { } else if ts, exists = wa.appStateFullSyncAttempted[evt.Name]; exists {
delete(wa.appStateFullSyncAttempted, evt.Name) delete(wa.appStateFullSyncAttempted, evt.Name)
@ -945,9 +924,6 @@ func (wa *WhatsAppClient) handleWAAppStateSyncError(ctx context.Context, evt *ev
if err != nil { if err != nil {
log.Err(err).Msg("Failed to save login metadata after marking app state recovery as attempted") 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() { go func() {
resp, err := wa.Client.SendPeerMessage(ctx, whatsmeow.BuildAppStateRecoveryRequest(evt.Name)) resp, err := wa.Client.SendPeerMessage(ctx, whatsmeow.BuildAppStateRecoveryRequest(evt.Name))
if err != nil { if err != nil {

View file

@ -49,7 +49,7 @@ func (wa *WhatsAppClient) makeEventSender(ctx context.Context, id types.JID) bri
} }
} }
return bridgev2.EventSender{ return bridgev2.EventSender{
IsFromMe: wa.IsOwnJID(id), IsFromMe: id.User == wa.GetStore().GetJID().User || id.User == wa.GetStore().GetLID().User,
Sender: waid.MakeUserID(id), Sender: waid.MakeUserID(id),
SenderLogin: waid.MakeUserLoginID(senderLoginJID), SenderLogin: waid.MakeUserLoginID(senderLoginJID),
} }
@ -60,25 +60,24 @@ func (wa *WhatsAppClient) messageIDToKey(id *waid.ParsedMessageID) *waCommon.Mes
RemoteJID: ptr.Ptr(id.Chat.String()), RemoteJID: ptr.Ptr(id.Chat.String()),
ID: ptr.Ptr(id.ID), 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) key.FromMe = ptr.Ptr(true)
} }
if id.Chat.Server != types.MessengerServer && id.Chat.Server != types.DefaultUserServer && if id.Chat.Server != types.MessengerServer && id.Chat.Server != types.DefaultUserServer && id.Chat.Server != types.HiddenUserServer && id.Chat.Server != types.BotServer {
id.Chat.Server != types.HiddenUserServer && id.Chat.Server != types.BotServer {
key.Participant = ptr.Ptr(id.Sender.String()) key.Participant = ptr.Ptr(id.Sender.String())
} }
return key return key
} }
func (wa *WhatsAppClient) maybeConvertJIDToLID(ctx context.Context, jid types.JID) types.JID { func (wa *WhatsAppClient) maybeConvertJIDToLID(ctx context.Context, chatJID types.JID) types.JID {
if jid.Server == types.DefaultUserServer { if chatJID.Server == types.HiddenUserServer {
if lidForPN, err := wa.GetStore().LIDs.GetLIDForPN(ctx, jid); err != nil { if pn, err := wa.GetStore().LIDs.GetPNForLID(ctx, chatJID); err != nil {
wa.UserLogin.Log.Err(err). wa.UserLogin.Log.Err(err).
Stringer("pn", jid). Stringer("lid", chatJID).
Msg("Failed to get LID for phone number chat") Msg("Failed to get phone number for LID chat")
} else if !lidForPN.IsEmpty() { } else if !pn.IsEmpty() {
return lidForPN return pn.ToNonAD()
} }
} }
return jid return chatJID
} }

View file

@ -1,212 +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 <https://www.gnu.org/licenses/>.
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
}

View file

@ -135,8 +135,7 @@ var (
_ bridgev2.LoginProcessWebAuthn = (*WALogin)(nil) _ bridgev2.LoginProcessWebAuthn = (*WALogin)(nil)
) )
const LoginConnectWait = 30 * time.Second const LoginConnectWait = 15 * time.Second
const LoginPairPhoneWait = 30 * time.Second
func (wl *WALogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) { func (wl *WALogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) {
wl.Main.firstClientConnectOnce.Do(wl.Main.onFirstClientConnect) wl.Main.firstClientConnectOnce.Do(wl.Main.onFirstClientConnect)
@ -194,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) { 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() err := wl.Client.Connect()
if err != nil { if err != nil {
wl.Log.Err(err).Msg("Failed to connect to WhatsApp for phone code login") wl.Log.Err(err).Msg("Failed to connect to WhatsApp for phone code login")
return nil, err return nil, err
} }
connectCtx, cancelConnect := context.WithTimeout(ctx, LoginConnectWait) err = wl.WaitForQRs.Wait(ctx)
err = wl.WaitForQRs.Wait(connectCtx)
cancelConnect()
if err != nil { if err != nil {
wl.Log.Warn().Err(err).Msg("Timed out waiting for connection") wl.Log.Warn().Err(err).Msg("Timed out waiting for connection")
return nil, fmt.Errorf("failed to wait for connection: %w", err) return nil, fmt.Errorf("failed to wait for connection: %w", err)
} }
pairCtx, cancelPair := context.WithTimeout(ctx, LoginPairPhoneWait) pairingCode, err := wl.Client.PairPhone(ctx, input["phone_number"], true, whatsmeow.PairClientChrome, "Chrome (Linux)")
defer cancelPair()
pairingCode, err := wl.Client.PairPhone(pairCtx, input["phone_number"], true, whatsmeow.PairClientChrome, "Chrome (Linux)")
if err != nil { if err != nil {
wl.Log.Err(err).Msg("Failed to request phone code login") wl.Log.Err(err).Msg("Failed to request phone code login")
if errors.Is(err, whatsmeow.ErrPhoneNumberTooShort) { if errors.Is(err, whatsmeow.ErrPhoneNumberTooShort) {

View file

@ -155,8 +155,8 @@ func (wa *WhatsAppClient) sendMediaRequestDirect(ctx context.Context, rawMsgID n
return wa.Client.SendMediaRetryReceipt(ctx, &types.MessageInfo{ return wa.Client.SendMediaRetryReceipt(ctx, &types.MessageInfo{
ID: msgID.ID, ID: msgID.ID,
MessageSource: types.MessageSource{ MessageSource: types.MessageSource{
IsFromMe: wa.IsOwnJID(msgID.Sender), IsFromMe: msgID.Sender.User == wa.JID.User,
IsGroup: msgID.Chat.Server != types.DefaultUserServer && msgID.Chat.Server != types.HiddenUserServer && msgID.Chat.Server != types.BotServer, IsGroup: msgID.Chat.Server != types.DefaultUserServer && msgID.Chat.Server != types.BotServer,
Sender: msgID.Sender, Sender: msgID.Sender,
Chat: msgID.Chat, Chat: msgID.Chat,
}, },

View file

@ -25,8 +25,6 @@ import (
"time" "time"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.mau.fi/util/exmaps"
"go.mau.fi/util/exsync"
"go.mau.fi/util/ptr" "go.mau.fi/util/ptr"
"go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types"
@ -64,13 +62,6 @@ func looksEmaily(str string) bool {
return false return false
} }
type cacheEntry struct {
jid types.JID
ts time.Time
}
var isOnWhatsappCache = exsync.NewMap[string, cacheEntry]()
func (wa *WhatsAppClient) validateIdentifer(ctx context.Context, number string) (types.JID, error) { func (wa *WhatsAppClient) validateIdentifer(ctx context.Context, number string) (types.JID, error) {
if strings.HasSuffix(number, "@"+types.BotServer) || strings.HasSuffix(number, "@"+types.HiddenUserServer) { if strings.HasSuffix(number, "@"+types.BotServer) || strings.HasSuffix(number, "@"+types.HiddenUserServer) {
return types.ParseJID(number) return types.ParseJID(number)
@ -85,8 +76,6 @@ func (wa *WhatsAppClient) validateIdentifer(ctx context.Context, number string)
return types.EmptyJID, ErrInputLooksLikeEmail return types.EmptyJID, ErrInputLooksLikeEmail
} else if wa.Client == nil || !wa.Client.IsLoggedIn() { } else if wa.Client == nil || !wa.Client.IsLoggedIn() {
return types.EmptyJID, bridgev2.ErrNotLoggedIn return types.EmptyJID, bridgev2.ErrNotLoggedIn
} else if entry, ok := isOnWhatsappCache.Get(number); ok && time.Since(entry.ts) < 4*time.Hour {
return entry.jid, nil
} else if resp, err := wa.Client.IsOnWhatsApp(ctx, []string{number}); err != nil { } else if resp, err := wa.Client.IsOnWhatsApp(ctx, []string{number}); err != nil {
return types.EmptyJID, fmt.Errorf("failed to check if number is on WhatsApp: %w", err) return types.EmptyJID, fmt.Errorf("failed to check if number is on WhatsApp: %w", err)
} else if len(resp) == 0 { } else if len(resp) == 0 {
@ -94,7 +83,6 @@ func (wa *WhatsAppClient) validateIdentifer(ctx context.Context, number string)
} else if !resp[0].IsIn { } 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) return types.EmptyJID, bridgev2.WrapRespErr(fmt.Errorf("the server said +%s is not on WhatsApp", resp[0].JID.User), mautrix.MNotFound)
} else { } else {
isOnWhatsappCache.Set(number, cacheEntry{resp[0].JID, time.Now()})
return resp[0].JID, nil return resp[0].JID, nil
} }
} }
@ -120,24 +108,16 @@ func (wa *WhatsAppConnector) ValidateUserID(id networkid.UserID) bool {
} }
} }
func (wa *WhatsAppClient) startChatPNToLID(ctx context.Context, jid types.JID) (types.JID, error) { func (wa *WhatsAppClient) startChatLIDToPN(ctx context.Context, jid types.JID) (types.JID, error) {
if jid.Server == types.DefaultUserServer { if jid.Server == types.HiddenUserServer {
lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, jid) pn, err := wa.GetStore().LIDs.GetPNForLID(ctx, jid)
if err != nil { if err != nil {
return jid, fmt.Errorf("failed to get lid for phone number: %w", err) return jid, fmt.Errorf("failed to get phone number for lid: %w", err)
} else if lid.IsEmpty() { } else if pn.IsEmpty() {
resp, err := wa.Client.GetUserInfo(ctx, []types.JID{jid}) // Don't allow starting chats with LIDs for now
if err != nil { return jid, fmt.Errorf("phone number not found")
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 pn, nil
return lid, nil
} }
return jid, nil return jid, nil
} }
@ -156,7 +136,7 @@ func (wa *WhatsAppClient) makeCreateChatResponse(ctx context.Context, jid, origJ
func (wa *WhatsAppClient) CreateChatWithGhost(ctx context.Context, ghost *bridgev2.Ghost) (*bridgev2.CreateChatResponse, error) { func (wa *WhatsAppClient) CreateChatWithGhost(ctx context.Context, ghost *bridgev2.Ghost) (*bridgev2.CreateChatResponse, error) {
origJID := waid.ParseUserID(ghost.ID) origJID := waid.ParseUserID(ghost.ID)
jid, err := wa.startChatPNToLID(ctx, origJID) jid, err := wa.startChatLIDToPN(ctx, origJID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -168,7 +148,7 @@ func (wa *WhatsAppClient) ResolveIdentifier(ctx context.Context, identifier stri
if err != nil { if err != nil {
return nil, err return nil, err
} }
jid, err := wa.startChatPNToLID(ctx, origJID) jid, err := wa.startChatLIDToPN(ctx, origJID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -208,7 +188,6 @@ func (wa *WhatsAppClient) getContactList(ctx context.Context, filter string, onl
return nil, err return nil, err
} }
resp := make([]*bridgev2.ResolveIdentifierResponse, 0, len(contacts)) resp := make([]*bridgev2.ResolveIdentifierResponse, 0, len(contacts))
addedIDs := make(exmaps.Set[types.JID])
for jid, contactInfo := range contacts { for jid, contactInfo := range contacts {
if onlyContacts && (contactInfo.FirstName == "" && contactInfo.FullName == "") { if onlyContacts && (contactInfo.FirstName == "" && contactInfo.FullName == "") {
continue continue
@ -216,43 +195,31 @@ func (wa *WhatsAppClient) getContactList(ctx context.Context, filter string, onl
if !matchesQuery(contactInfo.PushName, filter) && !matchesQuery(contactInfo.FullName, filter) && !matchesQuery(jid.User, filter) { if !matchesQuery(contactInfo.PushName, filter) && !matchesQuery(contactInfo.FullName, filter) && !matchesQuery(jid.User, filter) {
continue 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)) ghost, _ := wa.Main.Bridge.GetGhostByID(ctx, waid.MakeUserID(jid))
resp = append(resp, &bridgev2.ResolveIdentifierResponse{ resp = append(resp, &bridgev2.ResolveIdentifierResponse{
Ghost: ghost, Ghost: ghost,
UserID: waid.MakeUserID(jid), UserID: waid.MakeUserID(jid),
UserInfo: wa.contactToUserInfo(ctx, jid, contactInfo, false), UserInfo: wa.contactToUserInfo(ctx, jid, contactInfo, false),
Chat: chatResp, Chat: &bridgev2.CreateChatResponse{PortalKey: wa.makeWAPortalKey(jid)},
}) })
} }
return resp, nil return resp, nil
} }
func (wa *WhatsAppClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCreateParams) (*bridgev2.CreateChatResponse, error) { 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{ req := whatsmeow.ReqCreateGroup{
Name: ptr.Val(params.Name).Name, Name: ptr.Val(params.Name).Name,
Participants: make([]types.JID, len(params.Participants)), Participants: make([]types.JID, len(params.Participants)),
CreateKey: createKey,
} }
for i, participant := range params.Participants { for i, participant := range params.Participants {
jid := waid.ParseUserID(participant) 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 { if err != nil {
return nil, fmt.Errorf("failed to normalize participant %s: %w", participant, err) return nil, fmt.Errorf("failed to normalize participant %s: %w", participant, err)
} }

View file

@ -96,12 +96,12 @@ func (mq *MessageQuery) GetBetween(ctx context.Context, loginID networkid.UserLo
args := []any{mq.BridgeID, loginID, chatJID} args := []any{mq.BridgeID, loginID, chatJID}
argNum := 4 argNum := 4
if startTime != nil { if startTime != nil {
whereClauses += fmt.Sprintf(" AND timestamp > $%d", argNum) whereClauses += fmt.Sprintf(" AND timestamp >= $%d", argNum)
args = append(args, startTime.Unix()) args = append(args, startTime.Unix())
argNum++ argNum++
} }
if endTime != nil { if endTime != nil {
whereClauses += fmt.Sprintf(" AND timestamp < $%d", argNum) whereClauses += fmt.Sprintf(" AND timestamp <= $%d", argNum)
args = append(args, endTime.Unix()) args = append(args, endTime.Unix())
} }

View file

@ -1,4 +1,4 @@
-- v0 -> v10 (compatible with v3+): Latest revision -- v0 -> v9 (compatible with v3+): Latest revision
CREATE TABLE whatsapp_poll_option_id ( CREATE TABLE whatsapp_poll_option_id (
bridge_id TEXT NOT NULL, bridge_id TEXT NOT NULL,

View file

@ -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');

View file

@ -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');

View file

@ -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='';

View file

@ -6,9 +6,11 @@ import (
"go.mau.fi/util/dbutil" "go.mau.fi/util/dbutil"
) )
var Table dbutil.UpgradeTable
//go:embed *.sql //go:embed *.sql
var rawUpgrades embed.FS var rawUpgrades embed.FS
var Table = dbutil.BuildUpgradeTable(). func init() {
WithFS(rawUpgrades). Table.RegisterFS(rawUpgrades)
Finish() }

View file

@ -67,11 +67,10 @@ func getMessageType(waMsg *waE2E.Message) string {
return "encrypted comment" return "encrypted comment"
case waMsg.CommentMessage != nil: case waMsg.CommentMessage != nil:
return "comment" return "comment"
case waMsg.PollCreationMessage != nil || waMsg.PollCreationMessageV2 != nil || waMsg.PollCreationMessageV3 != nil || case waMsg.PollCreationMessage != nil || waMsg.PollCreationMessageV2 != nil || waMsg.PollCreationMessageV3 != nil:
waMsg.PollCreationMessageV5 != nil || waMsg.PollCreationMessageV6 != nil:
return "poll create" return "poll create"
case waMsg.PollCreationMessageV4 != nil: case waMsg.PollCreationMessageV4 != nil || waMsg.PollCreationMessageV5 != nil:
return "poll create (v4)" return "poll create (vNext)"
case waMsg.PollUpdateMessage != nil: case waMsg.PollUpdateMessage != nil:
return "poll update" return "poll update"
case waMsg.ProtocolMessage != nil: case waMsg.ProtocolMessage != nil:

View file

@ -143,14 +143,14 @@ func (mc *MessageConverter) ToWhatsApp(
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("failed to parse message ID: %w", err) return nil, nil, fmt.Errorf("failed to parse message ID: %w", err)
} }
rootMsgInfo := MessageIDToInfo(ctx, client, parsedID) rootMsgInfo := MessageIDToInfo(client, parsedID)
message, err = client.EncryptComment(ctx, rootMsgInfo, message) message, err = client.EncryptComment(ctx, rootMsgInfo, message)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("failed to encrypt comment: %w", err) return nil, nil, fmt.Errorf("failed to encrypt comment: %w", err)
} }
lid := parsedID.Sender lid := parsedID.Sender
if lid.Server == types.DefaultUserServer { 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 { if err != nil {
return nil, nil, fmt.Errorf("failed to get LID for PN: %w", err) return nil, nil, fmt.Errorf("failed to get LID for PN: %w", err)
} }

View file

@ -175,12 +175,6 @@ func (mc *MessageConverter) ToMatrix(
part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV2) part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV2)
case waMsg.PollCreationMessageV3 != nil: case waMsg.PollCreationMessageV3 != nil:
part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV3) part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV3)
//case waMsg.PollCreationMessageV4 != nil:
// part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV4)
case waMsg.PollCreationMessageV5 != nil:
part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV5)
case waMsg.PollCreationMessageV6 != nil:
part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV6)
case waMsg.PollUpdateMessage != nil: case waMsg.PollUpdateMessage != nil:
part, contextInfo = mc.convertPollUpdateMessage(ctx, info, waMsg.PollUpdateMessage) part, contextInfo = mc.convertPollUpdateMessage(ctx, info, waMsg.PollUpdateMessage)
case waMsg.EventMessage != nil: case waMsg.EventMessage != nil:
@ -271,26 +265,30 @@ func (mc *MessageConverter) ToMatrix(
if chat.IsEmpty() { if chat.IsEmpty() {
chat, _ = waid.ParsePortalID(portal.ID) 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{ cm.ReplyTo = &networkid.MessageOptionalPartID{
MessageID: waid.MakeMessageID(chat, pcp, contextInfo.GetStanzaID()), 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() { if contextInfo.GetIsForwarded() {
hasCaption := part.Content.FileName != "" && part.Content.FileName != part.Content.Body hasCaption := part.Content.FileName != "" && part.Content.FileName != part.Content.Body

View file

@ -115,7 +115,7 @@ func (mc *MessageConverter) PollVoteToWhatsApp(
zerolog.Ctx(ctx).Err(err).Msg("Failed to parse message ID") zerolog.Ctx(ctx).Err(err).Msg("Failed to parse message ID")
return nil, fmt.Errorf("failed to parse message ID") return nil, fmt.Errorf("failed to parse message ID")
} }
pollMsgInfo := MessageIDToInfo(ctx, client, parsedMsgID) pollMsgInfo := MessageIDToInfo(client, parsedMsgID)
pollMsgInfo.Type = "poll" pollMsgInfo.Type = "poll"
optionHashes := make([][]byte, 0, len(content.Response.Answers)) optionHashes := make([][]byte, 0, len(content.Response.Answers))
if pollMsg.Metadata.(*waid.MessageMetadata).IsMatrixPoll { if pollMsg.Metadata.(*waid.MessageMetadata).IsMatrixPoll {
@ -146,23 +146,13 @@ func (mc *MessageConverter) PollVoteToWhatsApp(
return &waE2E.Message{PollUpdateMessage: pollUpdate}, err return &waE2E.Message{PollUpdateMessage: pollUpdate}, err
} }
func MessageIDToInfo(ctx context.Context, client *whatsmeow.Client, parsedMsgID *waid.ParsedMessageID) *types.MessageInfo { func MessageIDToInfo(client *whatsmeow.Client, parsedMsgID *waid.ParsedMessageID) *types.MessageInfo {
chat := parsedMsgID.Chat
sender := parsedMsgID.Sender
if chat.Server == types.DefaultUserServer {
chatLID, _ := client.Store.LIDs.GetLIDForPN(ctx, chat)
senderLID, _ := client.Store.LIDs.GetLIDForPN(ctx, sender)
if !chatLID.IsEmpty() && !senderLID.IsEmpty() {
chat = chatLID
sender = senderLID
}
}
return &types.MessageInfo{ return &types.MessageInfo{
MessageSource: types.MessageSource{ MessageSource: types.MessageSource{
Chat: chat, Chat: parsedMsgID.Chat,
Sender: sender, Sender: parsedMsgID.Sender,
IsFromMe: sender.User == client.Store.GetLID().User || sender.User == client.Store.GetJID().User, IsFromMe: parsedMsgID.Sender.User == client.Store.GetLID().User || parsedMsgID.Sender.User == client.Store.GetJID().User,
IsGroup: chat.Server == types.GroupServer, IsGroup: parsedMsgID.Chat.Server == types.GroupServer,
}, },
ID: parsedMsgID.ID, ID: parsedMsgID.ID,
} }

View file

@ -94,7 +94,31 @@ func (mc *MessageConverter) convertPollCreationMessage(ctx context.Context, msg
}, msg.GetContextInfo() }, 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 { 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() sender = sender.ToNonAD()
var err error var err error
if !key.GetFromMe() { if !key.GetFromMe() {
@ -107,7 +131,7 @@ func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender
if sender.Server == types.LegacyUserServer { if sender.Server == types.LegacyUserServer {
sender.Server = types.DefaultUserServer 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 { 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, // 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 // so message key sender is the other user in the DM
@ -115,12 +139,8 @@ func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender
} else { } else {
// Message key is not from the sender, but message sender (containing key) is not me, // Message key is not from the sender, but message sender (containing key) is not me,
// so message key sender is 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 { } else {
zerolog.Ctx(ctx).Warn(). zerolog.Ctx(ctx).Warn().
Stringer("chat", chat). Stringer("chat", chat).
@ -137,6 +157,10 @@ func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender
chat = remoteJID chat = remoteJID
} }
} }
sender = rerouteMessageKey(
context.WithValue(ctx, contextKeyClient, client),
chat, sender, groupLIDAddressing,
)
return waid.MakeMessageID(chat, sender, key.GetID()) return waid.MakeMessageID(chat, sender, key.GetID())
} }

View file

@ -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)) 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 { 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)) return networkid.MessageID(fmt.Sprintf("fake:%s:%s:%s", chat.ToNonAD().String(), sender.ToNonAD().String(), data))
} }