diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index 4b3b934..3703df9 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -7,12 +7,10 @@ type: Bug --- - + - -* [ ] This is an actual bug, not just a setup issue (see the [troubleshooting docs](https://docs.mau.fi/bridges/general/troubleshooting.html) or ask in the Matrix room for setup help). -* [ ] I am certain that sufficient information is included. Ask in the Matrix room first if not. -* [ ] The bug is still present on the main branch. The `!wa version` command output is: `` +It's always best to ask in the Matrix room first, especially if you aren't sure +what details are needed. Issues with insufficient detail will likely just be +ignored or closed immediately. +--> diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index c9fef73..343443b 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -11,14 +11,14 @@ jobs: strategy: fail-fast: false matrix: - go-version: ["1.26", "1.27"] - name: Lint ${{ matrix.go-version == '1.27' && '(latest)' || '(old)' }} + go-version: ["1.25", "1.26"] + name: Lint ${{ matrix.go-version == '1.26' && '(latest)' || '(old)' }} steps: - - uses: actions/checkout@v7 + - uses: actions/checkout@v6 - name: Set up Go - uses: actions/setup-go@v7 + uses: actions/setup-go@v6 with: go-version: ${{ matrix.go-version }} cache: true diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d17865..5c95db4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,41 +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 - -* Added placeholder for group message history share notices. -* Fixed community spaces not being bridged properly in some cases. -* Fixed handling edits of HD media. -* Fixed duplicate message when a message from another own device is initially - undecryptable and later gets resolved. - -# v26.05 - -* Added support for importing sticker packs from WhatsApp. -* Added support for WhatsApp's new message edit encryption scheme. - -# v26.04 - -* Added support for @room mentions in both directions. -* Changed initial backfill to happen even if WhatsApp doesn't send full history. -* Fixed panic when handling updates to unknown polls from WhatsApp. -* Fixed some background loops not stopping when a user is logged out. - -# v26.03 - -* Added option to save outgoing messages in the database to allow encryption - retries to work across restarts. -* Fixed contact list API not returning some contacts. -* Fixed business template messages with media duplicating the text part. - # v26.02 * Bumped minimum Go version to 1.25. diff --git a/Dockerfile b/Dockerfile index e59d110..4efc9d5 100644 --- a/Dockerfile +++ b/Dockerfile @@ -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 @@ -6,7 +6,7 @@ COPY . /build WORKDIR /build RUN ./build.sh -FROM alpine:3.24 +FROM alpine:3.23 ENV UID=1337 \ GID=1337 diff --git a/Dockerfile.ci b/Dockerfile.ci index 0a73ff5..cb3be51 100644 --- a/Dockerfile.ci +++ b/Dockerfile.ci @@ -1,6 +1,6 @@ ARG DOCKER_HUB="docker.io" -FROM ${DOCKER_HUB}/alpine:3.24 +FROM ${DOCKER_HUB}/alpine:3.23 ENV UID=1337 \ GID=1337 diff --git a/cmd/mautrix-whatsapp/legacyprovision.go b/cmd/mautrix-whatsapp/legacyprovision.go new file mode 100644 index 0000000..f0527b2 --- /dev/null +++ b/cmd/mautrix-whatsapp/legacyprovision.go @@ -0,0 +1,153 @@ +package main + +import ( + "net/http" + "strings" + + "github.com/rs/zerolog/hlog" + "go.mau.fi/util/exhttp" + "go.mau.fi/whatsmeow" + "go.mau.fi/whatsmeow/appstate" + "go.mau.fi/whatsmeow/types" + "maunium.net/go/mautrix" + "maunium.net/go/mautrix/bridgev2" + "maunium.net/go/mautrix/bridgev2/matrix" + "maunium.net/go/mautrix/id" + + "go.mau.fi/mautrix-whatsapp/pkg/connector" + "go.mau.fi/mautrix-whatsapp/pkg/waid" +) + +type OtherUserInfo struct { + MXID id.UserID `json:"mxid"` + JID types.JID `json:"jid"` + Name string `json:"displayname"` + Avatar id.ContentURIString `json:"avatar_url"` +} + +type PortalInfo struct { + RoomID id.RoomID `json:"room_id"` + OtherUser *OtherUserInfo `json:"other_user,omitempty"` + GroupInfo *types.GroupInfo `json:"group_info,omitempty"` + JustCreated bool `json:"just_created"` +} + +type Error struct { + Success bool `json:"success"` + Error string `json:"error"` + ErrCode string `json:"errcode"` +} + +func legacyProvContacts(w http.ResponseWriter, r *http.Request) { + userLogin := m.Matrix.Provisioning.GetLoginForRequest(w, r) + if userLogin == nil { + return + } + if contacts, err := userLogin.Client.(*connector.WhatsAppClient).GetStore().Contacts.GetAllContacts(r.Context()); err != nil { + hlog.FromRequest(r).Err(err).Msg("Failed to fetch all contacts") + exhttp.WriteJSONResponse(w, http.StatusInternalServerError, Error{ + Error: "Internal server error while fetching contact list", + ErrCode: "failed to get contacts", + }) + } else { + augmentedContacts := map[types.JID]any{} + for jid, contact := range contacts { + var avatarURL id.ContentURIString + if puppet, _ := m.Bridge.GetExistingGhostByID(r.Context(), waid.MakeUserID(jid)); puppet != nil { + avatarURL = puppet.AvatarMXC + } + augmentedContacts[jid] = map[string]interface{}{ + "Found": contact.Found, + "FirstName": contact.FirstName, + "FullName": contact.FullName, + "PushName": contact.PushName, + "BusinessName": contact.BusinessName, + "AvatarURL": avatarURL, + } + } + exhttp.WriteJSONResponse(w, http.StatusOK, augmentedContacts) + } +} + +func legacyProvResolveIdentifier(w http.ResponseWriter, r *http.Request) { + number := r.PathValue("number") + userLogin := m.Matrix.Provisioning.GetLoginForRequest(w, r) + if userLogin == nil { + return + } + startChat := strings.Contains(r.URL.Path, "/v1/pm/") + resp, err := userLogin.Client.(*connector.WhatsAppClient).ResolveIdentifier(r.Context(), number, startChat) + if err != nil { + hlog.FromRequest(r).Warn().Err(err).Str("identifier", number).Msg("Failed to resolve identifier") + matrix.RespondWithError(w, err, "Internal error resolving identifier") + return + } + var portal *bridgev2.Portal + if startChat { + portal, err = m.Bridge.GetPortalByKey(r.Context(), resp.Chat.PortalKey) + if err != nil { + hlog.FromRequest(r).Warn().Err(err).Stringer("portal_key", resp.Chat.PortalKey).Msg("Failed to get portal by key") + matrix.RespondWithError(w, err, "Internal error getting portal by key") + return + } + err = portal.CreateMatrixRoom(r.Context(), userLogin, nil) + if err != nil { + hlog.FromRequest(r).Warn().Err(err).Stringer("portal_key", resp.Chat.PortalKey).Msg("Failed to create matrix room for portal") + matrix.RespondWithError(w, err, "Internal error creating matrix room for portal") + return + } + } else { + portal, _ = m.Bridge.GetExistingPortalByKey(r.Context(), resp.Chat.PortalKey) + } + var roomID id.RoomID + if portal != nil { + roomID = portal.MXID + } + exhttp.WriteJSONResponse(w, http.StatusOK, PortalInfo{ + RoomID: roomID, + OtherUser: &OtherUserInfo{ + JID: waid.ParseUserID(resp.UserID), + MXID: resp.Ghost.Intent.GetMXID(), + Name: resp.Ghost.Name, + Avatar: resp.Ghost.AvatarMXC, + }, + }) +} + +func provAppStateDebug(w http.ResponseWriter, r *http.Request) { + userLogin := m.Matrix.Provisioning.GetLoginForRequest(w, r) + if userLogin == nil { + return + } + client := userLogin.Client.(*connector.WhatsAppClient) + if client.Client == nil { + mautrix.MNotFound.WithMessage("WhatsApp client not connected").Write(w) + return + } + client.Client.AppStateDebugLogs = true + err := client.Client.FetchAppState(r.Context(), appstate.WAPatchName(r.PathValue("patch")), r.URL.Query().Get("full") == "1", false) + client.Client.AppStateDebugLogs = false + if err != nil { + mautrix.MUnknown.WithMessage("Failed to fetch app state: %v", err).Write(w) + } else { + exhttp.WriteEmptyJSONResponse(w, http.StatusOK) + } +} + +func provRecoverAppStateDebug(w http.ResponseWriter, r *http.Request) { + userLogin := m.Matrix.Provisioning.GetLoginForRequest(w, r) + if userLogin == nil { + return + } + client := userLogin.Client.(*connector.WhatsAppClient) + if client.Client == nil { + mautrix.MNotFound.WithMessage("WhatsApp client not connected").Write(w) + return + } + resp, err := client.Client.SendPeerMessage(r.Context(), whatsmeow.BuildAppStateRecoveryRequest(appstate.WAPatchName(r.PathValue("patch")))) + if err != nil { + mautrix.MUnknown.WithMessage("Failed to send app state recovery request: %v", err).Write(w) + } else { + exhttp.WriteJSONResponse(w, http.StatusOK, resp) + } +} diff --git a/cmd/mautrix-whatsapp/main.go b/cmd/mautrix-whatsapp/main.go index 011e9b0..3435150 100644 --- a/cmd/mautrix-whatsapp/main.go +++ b/cmd/mautrix-whatsapp/main.go @@ -18,12 +18,21 @@ var m = mxmain.BridgeMain{ Name: "mautrix-whatsapp", URL: "https://github.com/mautrix/whatsapp", Description: "A Matrix-WhatsApp puppeting bridge.", - Version: "26.08", + Version: "26.02", SemCalVer: true, Connector: &connector.WhatsAppConnector{}, } func main() { + m.PostStart = func() { + if m.Matrix.Provisioning != nil { + m.Matrix.Provisioning.Router.HandleFunc("GET /v1/contacts", legacyProvContacts) + m.Matrix.Provisioning.Router.HandleFunc("GET /v1/resolve_identifier/{number}", legacyProvResolveIdentifier) + m.Matrix.Provisioning.Router.HandleFunc("POST /v1/pm/{number}", legacyProvResolveIdentifier) + m.Matrix.Provisioning.Router.HandleFunc("POST /v1/debug/appstate/{patch}", provAppStateDebug) + m.Matrix.Provisioning.Router.HandleFunc("POST /v1/debug/recover-appstate/{patch}", provRecoverAppStateDebug) + } + } m.InitVersion(Tag, Commit, BuildTime) m.Run() } diff --git a/cmd/mautrix-whatsapp/plugin.go b/cmd/mautrix-whatsapp/plugin.go new file mode 100644 index 0000000..a1c9ca0 --- /dev/null +++ b/cmd/mautrix-whatsapp/plugin.go @@ -0,0 +1,24 @@ +//go:build amd64 && cgo && !noplugin + +package main + +import ( + "fmt" + "os" + "plugin" + + "go.mau.fi/util/exerrors" + + "go.mau.fi/mautrix-whatsapp/pkg/connector" +) + +func init() { + path := os.Getenv("WM_PLUGIN_PATH") + if path == "" { + return + } + fmt.Println("Loading plugin from", path) + plug := exerrors.Must(plugin.Open(path)) + sym := exerrors.Must(plug.Lookup("NewClient")) + connector.NewMC = sym.(connector.NewMCFunc) +} diff --git a/go.mod b/go.mod index f7a6451..2c90702 100644 --- a/go.mod +++ b/go.mod @@ -1,53 +1,53 @@ module go.mau.fi/mautrix-whatsapp -go 1.26.0 +go 1.25.0 -toolchain go1.27.0 +toolchain go1.26.0 tool go.mau.fi/util/cmd/maubuild require ( - github.com/lib/pq v1.12.3 - github.com/rs/zerolog v1.35.1 - github.com/tidwall/gjson v1.19.0 - go.mau.fi/util v0.10.1-0.20260820140024-eb612d936fde - go.mau.fi/webp v0.3.0 - go.mau.fi/whatsmeow v0.0.0-20260821141805-33cfac511629 - golang.org/x/image v0.45.0 - golang.org/x/net v0.58.0 - golang.org/x/sync v0.22.0 - google.golang.org/protobuf v1.36.12 + github.com/lib/pq v1.11.2 + github.com/rs/zerolog v1.34.0 + go.mau.fi/util v0.9.6 + go.mau.fi/webp v0.2.0 + go.mau.fi/whatsmeow v0.0.0-20260218131543-e4d82a04d5d8 + golang.org/x/image v0.36.0 + golang.org/x/net v0.50.0 + golang.org/x/sync v0.19.0 + google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 - maunium.net/go/mautrix v0.30.1-0.20260820142923-ae00f2c62e1e + maunium.net/go/mautrix v0.26.3 ) require ( - filippo.io/edwards25519 v1.2.0 // indirect + filippo.io/edwards25519 v1.1.0 // indirect github.com/beeper/argo-go v1.1.2 // indirect - github.com/coder/websocket v1.8.15 // indirect - github.com/coreos/go-systemd/v22 v22.7.0 // indirect + github.com/coder/websocket v1.8.14 // indirect + github.com/coreos/go-systemd/v22 v22.6.0 // indirect github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect github.com/google/uuid v1.6.0 // indirect github.com/kr/pretty v0.3.1 // indirect github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-sqlite3 v1.14.49 // indirect - github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b // indirect + github.com/mattn/go-sqlite3 v1.14.34 // indirect + github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/rs/xid v1.6.0 // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect + github.com/tidwall/gjson v1.18.0 // indirect github.com/tidwall/match v1.2.0 // indirect github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/sjson v1.2.5 // indirect github.com/vektah/gqlparser/v2 v2.5.27 // indirect - github.com/yuin/goldmark v1.8.5 // indirect - go.mau.fi/libsignal v0.2.2 // indirect + github.com/yuin/goldmark v1.7.16 // indirect + go.mau.fi/libsignal v0.2.1 // indirect go.mau.fi/zeroconfig v0.2.0 // indirect - golang.org/x/crypto v0.55.0 // indirect - golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 // indirect - golang.org/x/mod v0.40.0 // indirect - golang.org/x/sys v0.47.0 // indirect - golang.org/x/text v0.41.0 // indirect + golang.org/x/crypto v0.48.0 // indirect + golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a // indirect + golang.org/x/mod v0.33.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect maunium.net/go/mauflag v1.0.0 // indirect diff --git a/go.sum b/go.sum index 1adbb52..c03e9ee 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,5 @@ -filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= -filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= +filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA= +filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4= github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU= github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= @@ -8,15 +8,17 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNg github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/beeper/argo-go v1.1.2 h1:UQI2G8F+NLfGTOmTUI0254pGKx/HUU/etbUGTJv91Fs= github.com/beeper/argo-go v1.1.2/go.mod h1:M+LJAnyowKVQ6Rdj6XYGEn+qcVFkb3R/MUpqkGR0hM4= -github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA= -github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= -github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= -github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= +github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g= +github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo= +github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg= github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= @@ -28,17 +30,21 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= -github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs= +github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-sqlite3 v1.14.49 h1:B8jBHC3xhxZgxztrgruTuLucebnULQnx4W7cF7SAE9w= -github.com/mattn/go-sqlite3 v1.14.49/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= -github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b h1:sS7HLzwS+dO+gxATgQfeZDEdUZe2pKAB3nGoUwP5zU0= -github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= +github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk= +github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y= +github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 h1:KPpdlQLZcHfTMQRi6bFQ7ogNO0ltFT4PmtwTLW4W+14= +github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= @@ -46,8 +52,8 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= -github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= -github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= +github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY= +github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= @@ -55,8 +61,8 @@ github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDq github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= -github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU= -github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= +github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY= +github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= @@ -67,37 +73,39 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s= github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= -github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= -github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= -go.mau.fi/libsignal v0.2.2 h1:QV+XdzQkm3x3aSG7FcqfGSZuFXz83pRZPBFaPygHbOU= -go.mau.fi/libsignal v0.2.2/go.mod h1:CRlIQg2J8uYTfDFvNoO8/KcZjs5cey0vbc6oj/bssY0= -go.mau.fi/util v0.10.1-0.20260820140024-eb612d936fde h1:eMHY9dMDkNuDMWhfTbMZHbbsxj7G6mfujjKei1HaFQM= -go.mau.fi/util v0.10.1-0.20260820140024-eb612d936fde/go.mod h1:z0ZZNt4hq3FZbUKnunexE/QscCx7VkLvQSvtggc/aE8= -go.mau.fi/webp v0.3.0 h1:gVHQZtz21Ziwj+CDuklbX9mqpsnDIFKxs/BJyV7iZzA= -go.mau.fi/webp v0.3.0/go.mod h1:rlZFTev+dYxhvk+XNBP/5GcTt4gXmzAB4DU0aGUYIQo= -go.mau.fi/whatsmeow v0.0.0-20260821141805-33cfac511629 h1:99p9fTS1G73a1aifCVaV5MX4hDq0lVUKWGoUNoXyE3A= -go.mau.fi/whatsmeow v0.0.0-20260821141805-33cfac511629/go.mod h1:aMd13H2xFFGH9cskcvxo4Aae+TmyFN38yw+HvsrpwVg= +github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE= +github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= +go.mau.fi/libsignal v0.2.1 h1:vRZG4EzTn70XY6Oh/pVKrQGuMHBkAWlGRC22/85m9L0= +go.mau.fi/libsignal v0.2.1/go.mod h1:iVvjrHyfQqWajOUaMEsIfo3IqgVMrhWcPiiEzk7NgoU= +go.mau.fi/util v0.9.6 h1:2nsvxm49KhI3wrFltr0+wSUBlnQ4CMtykuELjpIU+ts= +go.mau.fi/util v0.9.6/go.mod h1:sIJpRH7Iy5Ad1SBuxQoatxtIeErgzxCtjd/2hCMkYMI= +go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg= +go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q= +go.mau.fi/whatsmeow v0.0.0-20260218131543-e4d82a04d5d8 h1:riEnRpKjNnVLuaGIm+9Q3SozatMsseJLxnc3ZWH6Exo= +go.mau.fi/whatsmeow v0.0.0-20260218131543-e4d82a04d5d8/go.mod h1:mXCRFyPEPn4jqWz6Afirn8vY7DpHCPnlKq6I2cWwFHM= go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU= go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w= -golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= -golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= -golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 h1:YXnL44eJ77R+ji4/ooy8UsXIhz+lbi2Qgdlc8iRN0gY= -golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297/go.mod h1:Mkmymgv+uMpSQ/XxJ/7GpdrdYoqm3u72jEbpCLiJmNk= -golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0= -golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4= -golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= -golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= -golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= -golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= -golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= -golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o= +golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA= +golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc= +golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4= +golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8= +golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w= +golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/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.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= -golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= -google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= -google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= @@ -107,5 +115,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= -maunium.net/go/mautrix v0.30.1-0.20260820142923-ae00f2c62e1e h1:ATvERyBIUXJrnL7ezP2xCLLGCNmGClmfzoQq4N3jBcA= -maunium.net/go/mautrix v0.30.1-0.20260820142923-ae00f2c62e1e/go.mod h1:Y02sBiAvfEVqK24bwVGCprmLATRZ7prWel3ZpB413e0= +maunium.net/go/mautrix v0.26.3 h1:tWZih6Vjw0qGTWuPmg9JUrQPzViTNDPGQLVc5UXC4nk= +maunium.net/go/mautrix v0.26.3/go.mod h1:v5ZdDoCwUpNqEj5OrhEoUa3L1kEddKPaAya9TgGXN38= diff --git a/pkg/connector/backfill.go b/pkg/connector/backfill.go index bb555e6..f05c0f2 100644 --- a/pkg/connector/backfill.go +++ b/pkg/connector/backfill.go @@ -12,7 +12,6 @@ import ( "time" "github.com/rs/zerolog" - "go.mau.fi/util/exmaps" "go.mau.fi/util/ptr" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/proto/waE2E" @@ -64,6 +63,11 @@ func (wa *WhatsAppClient) historySyncLoop(ctx context.Context) { for { var resetTimer bool select { + case evt := <-wa.historySyncs: + // The timer is stopped unconditionally and restarted if either handleWAHistorySync had conversations, + // or if the timer was previously started and hadn't reached the loop above yet. + dispatchTimer.Stop() + resetTimer, _ = wa.handleWAHistorySync(ctx, evt, false) case <-wa.historySyncWakeup: dispatchTimer.Stop() notif, rowid, err := wa.Main.DB.HSNotif.GetNext(ctx, wa.UserLogin.ID) @@ -116,30 +120,14 @@ func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context, Uint32("chunk_order", evt.GetChunkOrder()). Uint32("progress", evt.GetProgress()). Logger() - log.Debug(). - Int64("oldest_msg_in_chunk_ts", evt.GetOldestMsgInChunkTimestampSec()). - Any("full_request_meta", evt.GetFullHistorySyncOnDemandRequestMetadata()). - Any("access_status", evt.GetMessageAccessStatus()). - Str("peer_data_request_session_id", evt.GetPeerDataRequestSessionID()). - Msg("Downloading history sync") + log.Debug().Msg("Downloading history sync") blob, err := wa.Client.DownloadHistorySync(log.WithContext(ctx), evt, true) if err != nil { log.Err(err).Msg("Failed to download history sync") return } - if blob.GetSyncType() == waHistorySync.HistorySync_ON_DEMAND { - wa.handleOnDemandHistorySync(ctx, blob) - if err = wa.Main.DB.HSNotif.Delete(ctx, rowid); err != nil { - log.Err(err).Msg("Failed to delete queued on-demand history sync notification") - } else if err = wa.Client.DeleteMedia(ctx, whatsmeow.MediaHistory, evt.GetDirectPath(), evt.GetFileEncSHA256(), evt.GetEncHandle()); err != nil { - log.Err(err).Msg("Failed to delete history sync blob from server") - } else { - log.Debug().Msg("Finished handling on-demand history sync and deleted history sync blob from server") - } - return - } err = wa.Main.DB.DoTxn(ctx, nil, func(ctx context.Context) (innerErr error) { - innerErr = wa.handleWAHistorySync(ctx, evt, blob, true) + resetTimer, innerErr = wa.handleWAHistorySync(ctx, blob, true) if innerErr != nil { return } @@ -151,28 +139,13 @@ func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context, }) if err != nil { log.Err(err).Msg("Failed to store history sync notification data") - } else { - resetTimer = blob.GetSyncType() == waHistorySync.HistorySync_INITIAL_BOOTSTRAP || - blob.GetSyncType() == waHistorySync.HistorySync_RECENT || - blob.GetSyncType() == waHistorySync.HistorySync_FULL - err = wa.Client.DeleteMedia(ctx, whatsmeow.MediaHistory, evt.GetDirectPath(), evt.GetFileEncSHA256(), evt.GetEncHandle()) - if err != nil { - log.Err(err).Msg("Failed to delete history sync blob from server") - } else { - log.Debug().Msg("Deleted history sync blob from server") - } } return } -func (wa *WhatsAppClient) handleWAHistorySync( - ctx context.Context, - notif *waE2E.HistorySyncNotification, - evt *waHistorySync.HistorySync, - stopOnError bool, -) error { +func (wa *WhatsAppClient) handleWAHistorySync(ctx context.Context, evt *waHistorySync.HistorySync, stopOnError bool) (bool, error) { if evt == nil || evt.SyncType == nil { - return nil + return false, nil } log := wa.UserLogin.Log.With(). Str("action", "store history sync"). @@ -197,16 +170,11 @@ func (wa *WhatsAppClient) handleWAHistorySync( Int("recent_sticker_count", len(evt.GetRecentStickers())). Int("past_participant_count", len(evt.GetPastParticipants())). Msg("Ignoring history sync") - return nil + return false, nil } log.Info(). Int("conversation_count", len(evt.GetConversations())). Int("past_participant_count", len(evt.GetPastParticipants())). - Dict("notification_metadata", zerolog.Dict(). - Int64("oldest_msg_in_chunk_ts", notif.GetOldestMsgInChunkTimestampSec()). - Any("full_request_meta", notif.GetFullHistorySyncOnDemandRequestMetadata()). - Any("access_status", notif.GetMessageAccessStatus()). - Str("peer_data_request_session_id", notif.GetPeerDataRequestSessionID())). Msg("Storing history sync") start := time.Now() successfullySavedTotal := 0 @@ -229,32 +197,30 @@ func (wa *WhatsAppClient) handleWAHistorySync( } else { totalMessageCount += len(conv.GetMessages()) } - if jid.Server == types.DefaultUserServer { - lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, jid) + if jid.Server == types.HiddenUserServer { + pn, err := wa.GetStore().LIDs.GetPNForLID(ctx, jid) if err != nil { - log.Err(err).Stringer("pn", jid).Msg("Failed to get LID for phone number in history sync") - } else if lid.IsEmpty() { - log.Warn().Stringer("pn", jid).Msg("No LID found for phone number in history sync") + log.Err(err).Stringer("lid", jid).Msg("Failed to get PN for LID in history sync") + } else if pn.IsEmpty() { + log.Warn().Stringer("lid", jid).Msg("No PN found for LID in history sync") } else { log.Debug(). - Stringer("lid", lid). - Stringer("pn", jid). - Msg("Rerouting phone number DM to LID in history sync") - jid = lid + Stringer("lid", jid). + Stringer("pn", pn). + Msg("Rerouting LID DM to phone number in history sync") + jid = pn } } log.UpdateContext(func(c zerolog.Context) zerolog.Context { return c.Stringer("chat_jid", jid) }) - var firstItemTime, lastItemTime time.Time + var minTime, maxTime time.Time + var minTimeIndex, maxTimeIndex int ignoredTypes := 0 - rawMessages := conv.GetMessages() - messages := make([]*wadb.HistorySyncMessageTuple, 0, len(rawMessages)) - allowClamp := conv.GetCommentsCount() == 0 - var newerTS uint64 - for i, rawMsg := range rawMessages { + messages := make([]*wadb.HistorySyncMessageTuple, 0, len(conv.GetMessages())) + for i, rawMsg := range conv.GetMessages() { // Don't store messages that will just be skipped. msgEvt, err := wa.Client.ParseWebMessage(jid, rawMsg.GetMessage()) if err != nil { @@ -263,39 +229,22 @@ func (wa *WhatsAppClient) handleWAHistorySync( Str("msg_id", rawMsg.GetMessage().GetKey().GetID()). Uint64("msg_time_seconds", rawMsg.GetMessage().GetMessageTimestamp()). Msg("Dropping historical message due to parse error") - log.Trace(). - Any("web_message_info", rawMsg.GetMessage()). - Msg("Content of event that failed to parse") continue } - if firstItemTime.IsZero() { - firstItemTime = 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 } - lastItemTime = msgEvt.Info.Timestamp msgType := getMessageType(msgEvt.Message) if msgType == "ignore" || strings.HasPrefix(msgType, "unknown_protocol_") { ignoredTypes++ continue } - // Comments (replies) in announcement groups are not ordered by timestamp, so don't clamp them. - if rawMsg.GetMessage().GetCommentMetadata().GetCommentParentKey() != nil { - allowClamp = false - } - // WhatsApp has bugs where some random messages will have timestamps decades in the future. - // To ensure they don't mess up our ordering, require timestamps of older messages to be - // before the previous (newer) message. - if currentTS := rawMsg.GetMessage().GetMessageTimestamp(); newerTS > 0 && allowClamp && currentTS > newerTS { - log.Warn(). - Time("current_ts", time.Unix(int64(currentTS), 0)). - Time("prev_ts", time.Unix(int64(newerTS), 0)). - Int("msg_index", i). - Str("msg_id", rawMsg.GetMessage().GetKey().GetID()). - Msg("Clamping message timestamp") - rawMsg.Message.MessageTimestamp = ptr.Ptr(newerTS) - msgEvt.Info.Timestamp = time.Unix(int64(newerTS), 0) - } - newerTS = rawMsg.GetMessage().GetMessageTimestamp() marshaled, err := proto.Marshal(rawMsg) if err != nil { log.Warn().Err(err). @@ -310,8 +259,10 @@ func (wa *WhatsAppClient) handleWAHistorySync( log.Debug(). Int("wrapped_count", len(messages)). Int("ignored_msg_type_count", ignoredTypes). - Time("first_item_time", firstItemTime). - Time("last_item_time", lastItemTime). + Time("lowest_time", minTime). + Int("lowest_time_index", minTimeIndex). + Time("highest_time", maxTime). + Int("highest_time_index", maxTimeIndex). Dict("metadata", zerolog.Dict(). Uint32("ephemeral_expiration", conv.GetEphemeralExpiration()). Int64("ephemeral_setting_timestamp", conv.GetEphemeralSettingTimestamp()). @@ -320,17 +271,15 @@ func (wa *WhatsAppClient) handleWAHistorySync( Bool("archived", conv.GetArchived()). Uint32("pinned", conv.GetPinned()). Uint64("mute_end", conv.GetMuteEndTime()). - Uint32("unread_count", conv.GetUnreadCount()). - Bool("end_of_history", conv.GetEndOfHistoryTransfer()). - Stringer("end_of_history_type", conv.GetEndOfHistoryTransferType()), + Uint32("unread_count", conv.GetUnreadCount()), ). Msg("Collected messages to save from history sync conversation") if len(messages) > 0 { - err = wa.Main.DB.Conversation.Put(ctx, wadb.NewConversation(wa.UserLogin.ID, jid, conv, firstItemTime)) + err = wa.Main.DB.Conversation.Put(ctx, wadb.NewConversation(wa.UserLogin.ID, jid, conv, maxTime)) if err != nil { if stopOnError { - return fmt.Errorf("failed to save conversation metadata for %s: %w", jid, err) + return false, fmt.Errorf("failed to save conversation metadata for %s: %w", jid, err) } log.Err(err).Msg("Failed to save conversation metadata") continue @@ -338,7 +287,7 @@ func (wa *WhatsAppClient) handleWAHistorySync( err = wa.Main.DB.Message.Put(ctx, wa.UserLogin.ID, jid, messages) if err != nil { if stopOnError { - return fmt.Errorf("failed to save messages in %s: %w", jid, err) + return false, fmt.Errorf("failed to save messages in %s: %w", jid, err) } log.Err(err).Msg("Failed to save messages") failedToSaveTotal += len(messages) @@ -348,7 +297,7 @@ func (wa *WhatsAppClient) handleWAHistorySync( err = wa.Main.Bridge.DB.BackfillTask.MarkNotDone(ctx, wa.makeWAPortalKey(jid), wa.UserLogin.ID) if err != nil { if stopOnError { - return fmt.Errorf("failed to mark backfill task as not done for %s: %w", jid, err) + return false, fmt.Errorf("failed to mark backfill task as not done for %s: %w", jid, err) } log.Err(err).Msg("Failed to mark backfill task as not done") } @@ -360,7 +309,9 @@ func (wa *WhatsAppClient) handleWAHistorySync( Int("total_message_count", totalMessageCount). Dur("duration", time.Since(start)). Msg("Finished storing history sync") - return nil + resetTimer := evt.GetSyncType() == waHistorySync.HistorySync_RECENT || + evt.GetSyncType() == waHistorySync.HistorySync_FULL + return resetTimer, nil } func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) { @@ -408,8 +359,8 @@ func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) { return } wrappedInfo, err := wa.getChatInfo(ctx, conv.ChatJID, conv, true) - if errors.Is(err, whatsmeow.ErrNotInGroup) || errors.Is(err, whatsmeow.ErrGroupNotFound) { - log.Debug().Err(err).Stringer("chat_jid", conv.ChatJID). + if errors.Is(err, whatsmeow.ErrNotInGroup) { + log.Debug().Stringer("chat_jid", conv.ChatJID). Msg("Skipping creating room because the user is not a participant") //err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, conv.ChatJID) //if err != nil { @@ -482,72 +433,40 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet if err != nil { return nil, err } - if portalJID.Server == types.DefaultUserServer { - zerolog.Ctx(ctx).Warn().Stringer("portal_jid", portalJID).Msg("FetchMessages called for phone number portal") - } var markRead bool var startTime, endTime *time.Time - var conv *wadb.Conversation - if params.Forward || wa.Main.Config.HistorySync.BackwardsOnDemand { - conv, err = wa.Main.DB.Conversation.Get(ctx, wa.UserLogin.ID, portalJID) - if err != nil { - return nil, fmt.Errorf("failed to get conversation from database: %w", err) - } - } if params.Forward { if params.AnchorMessage != nil { startTime = ptr.Ptr(params.AnchorMessage.Timestamp) } - if conv != nil { + conv, err := wa.Main.DB.Conversation.Get(ctx, wa.UserLogin.ID, portalJID) + if err != nil { + return nil, fmt.Errorf("failed to get conversation from database: %w", err) + } else if conv != nil { markRead = !ptr.Val(conv.MarkedAsUnread) && ptr.Val(conv.UnreadCount) == 0 } - } else { - if params.AnchorMessage != nil { - endTime = ptr.Ptr(params.AnchorMessage.Timestamp) + } else if params.Cursor != "" { + endTimeUnix, err := strconv.ParseInt(string(params.Cursor), 10, 64) + if err != nil { + return nil, fmt.Errorf("failed to parse cursor: %w", err) } - if params.Cursor != "" { - endTimeUnix, err := strconv.ParseInt(string(params.Cursor), 10, 64) - if err != nil { - return nil, fmt.Errorf("failed to parse cursor: %w", err) - } - cursorTime := time.Unix(endTimeUnix, 0) - if endTime == nil || cursorTime.Before(*endTime) { - endTime = &cursorTime - } - } - } - var anchorID types.MessageID - if params.AnchorMessage != nil { - parsedID, _ := waid.ParseMessageID(params.AnchorMessage.ID) - if parsedID != nil { - anchorID = parsedID.ID - } - } - var hasMore bool - if !params.Forward && wa.Main.Config.HistorySync.BackwardsOnDemand { - hasMore = conv != nil && ptr.Val(conv.EndOfHistoryTransferType) == waHistorySync.Conversation_COMPLETE_BUT_MORE_MESSAGES_REMAIN_ON_PRIMARY + endTime = ptr.Ptr(time.Unix(endTimeUnix, 0)) + } else if params.AnchorMessage != nil { + endTime = ptr.Ptr(params.AnchorMessage.Timestamp) } messages, err := wa.Main.DB.Message.GetBetween(ctx, wa.UserLogin.ID, portalJID, startTime, endTime, params.Count+1) if err != nil { return nil, fmt.Errorf("failed to load messages from database: %w", err) - } else if len(messages) == 0 || (len(messages) == 1 && anchorID != "" && messages[0].GetKey().GetID() == anchorID) { - wa.deleteHistorySyncMessages(ctx, portalJID, 0, 0) - if hasMore && !params.AllowSlowFetch { - return &bridgev2.FetchMessagesResponse{ - MoreRequiresSlowFetch: true, - HasMore: true, - Forward: params.Forward, - }, nil - } else if hasMore { - return wa.fetchMessagesFromPhone(ctx, portalJID, params) - } + } else if len(messages) == 0 { return &bridgev2.FetchMessagesResponse{ HasMore: false, Forward: params.Forward, }, nil } + hasMore := false + oldestTS := messages[len(messages)-1].GetMessageTimestamp() + newestTS := messages[0].GetMessageTimestamp() if len(messages) > params.Count { - oldestTS := messages[len(messages)-1].GetMessageTimestamp() hasMore = true // For safety, cut off messages with the oldest timestamp in the response. // Otherwise, if there are multiple messages with the same timestamp, the next fetch may miss some. @@ -558,90 +477,19 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet } } } - resp, err := wa.convertHistorySyncMessages(ctx, params.Portal, portalJID, messages, true) - if err != nil { - return nil, fmt.Errorf("failed to convert messages: %w", err) - } - resp.HasMore = hasMore - resp.Forward = params.Forward - resp.MarkRead = markRead - return resp, nil -} - -func (wa *WhatsAppClient) deleteHistorySyncMessages(ctx context.Context, portalJID types.JID, newestTS, oldestTS uint64) { - var err error - var rows int64 - if (newestTS == 0 && oldestTS == 0) || !wa.Main.Bridge.Config.Backfill.Queue.AnyEnabled() { - // If the backfill queue isn't enabled, delete all messages after backfilling a batch. - rows, err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, portalJID) - } else { - // Otherwise just delete the messages that got backfilled - rows, err = wa.Main.DB.Message.DeleteBetween(ctx, wa.UserLogin.ID, portalJID, newestTS, oldestTS) - } - if err != nil { - zerolog.Ctx(ctx).Warn().Err(err). - Stringer("portal_jid", portalJID). - Uint64("newest_ts", newestTS). - Uint64("oldest_ts", oldestTS). - Msg("Failed to delete messages from database after backfill") - } else { - zerolog.Ctx(ctx).Debug(). - Stringer("portal_jid", portalJID). - Uint64("newest_ts", newestTS). - Uint64("oldest_ts", oldestTS). - Int64("rows_affected", rows). - Msg("Deleted history sync messages from database") - } -} - -func (wa *WhatsAppClient) convertHistorySyncMessages( - ctx context.Context, - portal *bridgev2.Portal, - portalJID types.JID, - messages []*waWeb.WebMessageInfo, - explodeOnError bool, -) (*bridgev2.FetchMessagesResponse, error) { - oldestTS := messages[len(messages)-1].GetMessageTimestamp() - newestTS := messages[0].GetMessageTimestamp() - convertedMessages := make([]*bridgev2.BackfillMessage, 0, len(messages)) + convertedMessages := make([]*bridgev2.BackfillMessage, len(messages)) var mediaRequests []*wadb.MediaRequest - dups := make(exmaps.Set[networkid.MessageID]) for i, msg := range messages { evt, err := wa.Client.ParseWebMessage(portalJID, msg) if err != nil { - if explodeOnError { - // This should never happen because the info is already parsed once before being stored in the database - return nil, fmt.Errorf("failed to parse info of message %s: %w", msg.GetKey().GetID(), err) - } - zerolog.Ctx(ctx).Warn().Err(err). - Int("msg_index", i). - Str("msg_id", msg.GetKey().GetID()). - Uint64("msg_time_seconds", msg.GetMessageTimestamp()). - Msg("Dropping historical message due to parse error") - zerolog.Ctx(ctx).Trace(). - Any("web_message_info", msg.GetMessage()). - Msg("Content of event that failed to parse") - continue - } - if !explodeOnError { - msgType := getMessageType(evt.Message) - if msgType == "ignore" || strings.HasPrefix(msgType, "unknown_protocol_") { - continue - } - } - if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, false) { - return nil, fmt.Errorf("failed to ensure alt JIDs for message %s", evt.Info.ID) + // This should never happen because the info is already parsed once before being stored in the database + return nil, fmt.Errorf("failed to parse info of message %s: %w", msg.GetKey().GetID(), err) } + var mediaReq *wadb.MediaRequest isViewOnce := evt.IsViewOnce || evt.IsViewOnceV2 || evt.IsViewOnceV2Extension - converted, mediaReq := wa.convertHistorySyncMessage( - ctx, portal, &evt.Info, evt.Message, evt.RawMessage, isViewOnce, msg.Reactions, + convertedMessages[i], mediaReq = wa.convertHistorySyncMessage( + ctx, params.Portal, &evt.Info, evt.Message, evt.RawMessage, isViewOnce, msg.Reactions, ) - // This is a hack to remove duplicates where the same message is inserted with both the LID and phone number sender - // TODO prevent those being inserted in the first place instead of hacking around it here - if !dups.Add(converted.ID) { - continue - } - convertedMessages = append(convertedMessages, converted) if mediaReq != nil { mediaRequests = append(mediaRequests, mediaReq) } @@ -650,10 +498,24 @@ func (wa *WhatsAppClient) convertHistorySyncMessages( return &bridgev2.FetchMessagesResponse{ Messages: convertedMessages, Cursor: networkid.PaginationCursor(strconv.FormatUint(oldestTS, 10)), + HasMore: hasMore, + Forward: endTime == nil, + MarkRead: markRead, + // TODO set remaining or total count CompleteCallback: func() { // TODO this only deletes after backfilling. If there's no need for backfill after a relogin, // the messages will be stuck in the database - wa.deleteHistorySyncMessages(ctx, portalJID, newestTS, oldestTS) + var err error + if !wa.Main.Bridge.Config.Backfill.Queue.Enabled && !wa.Main.Bridge.Config.Backfill.WillPaginateManually { + // If the backfill queue isn't enabled, delete all messages after backfilling a batch. + err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, portalJID) + } else { + // Otherwise just delete the messages that got backfilled + err = wa.Main.DB.Message.DeleteBetween(ctx, wa.UserLogin.ID, portalJID, newestTS, oldestTS) + } + if err != nil { + zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to delete messages from database after backfill") + } if len(mediaRequests) > 0 { go func(ctx context.Context) { for _, req := range mediaRequests { @@ -671,109 +533,6 @@ func (wa *WhatsAppClient) convertHistorySyncMessages( }, nil } -func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, portalJID types.JID, params bridgev2.FetchMessagesParams) (*bridgev2.FetchMessagesResponse, error) { - if params.AnchorMessage == nil { - return nil, fmt.Errorf("anchor message is required to fetch messages from phone") - } - parsed, err := waid.ParseMessageID(params.AnchorMessage.ID) - if err != nil { - return nil, fmt.Errorf("failed to parse anchor message ID: %w", err) - } - - msgID := wa.Client.GenerateMessageID() - reqData := wa.Client.BuildHistorySyncRequest(&types.MessageInfo{ - MessageSource: types.MessageSource{ - Chat: portalJID, - Sender: parsed.Sender, - IsFromMe: wa.IsOwnJID(parsed.Sender), - IsGroup: parsed.Chat.Server == types.GroupServer, - }, - ID: parsed.ID, - Timestamp: params.AnchorMessage.Timestamp, - }, 50) - zerolog.Ctx(ctx).Debug(). - Str("request_msg_id", msgID). - Stringer("portal_jid", portalJID). - Any("anchor_msg_parsed", parsed). - Any("request_data", reqData). - Msg("Sending history sync request") - _, err = wa.Client.SendMessage(ctx, wa.JID.ToNonAD(), reqData, whatsmeow.SendRequestExtra{ - ID: msgID, - Peer: true, - }) - if err != nil { - return nil, fmt.Errorf("failed to send history sync request: %w", err) - } - return &bridgev2.FetchMessagesResponse{ - HasMore: true, - Pending: true, - }, nil -} - -func (wa *WhatsAppClient) handleOnDemandHistorySync(ctx context.Context, blob *waHistorySync.HistorySync) { - if len(blob.GetConversations()) > 1 { - zerolog.Ctx(ctx).Warn(). - Int("conversation_count", len(blob.GetConversations())). - Msg("Received on-demand history sync with multiple conversations") - } - for _, conv := range blob.GetConversations() { - portalJID, err := types.ParseJID(conv.GetID()) - if err != nil { - zerolog.Ctx(ctx).Err(err).Str("jid", conv.GetID()).Msg("Failed to parse portal JID") - continue - } - if portalJID.Server == types.DefaultUserServer { - lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, portalJID) - if err != nil { - zerolog.Ctx(ctx).Err(err).Stringer("lid", portalJID).Msg("Failed to get LID for phone number in on-demand history sync") - } else if lid.IsEmpty() { - zerolog.Ctx(ctx).Warn().Stringer("lid", portalJID).Msg("No LID found for phone number in on-demand history sync") - } else { - zerolog.Ctx(ctx).Debug(). - Stringer("lid", lid). - Stringer("pn", portalJID). - Msg("Rerouting phone number DM to LID in on-demand history sync") - portalJID = lid - } - } - portal, err := wa.Main.Bridge.GetPortalByKey(ctx, wa.makeWAPortalKey(portalJID)) - if err != nil { - zerolog.Ctx(ctx).Err(err).Stringer("portal_jid", portalJID).Msg("Failed to get portal for on-demand history sync") - continue - } - ctx := zerolog.Ctx(ctx).With(). - Str("portal_id", string(portal.ID)). - Str("portal_receiver", string(portal.Receiver)). - Stringer("portal_mxid", portal.MXID). - Logger().WithContext(ctx) - portal.HandleRemoteBackfill(ctx, wa.UserLogin, &simplevent.Backfill{ - EventMeta: simplevent.EventMeta{ - Type: bridgev2.RemoteEventBackfill, - PortalKey: portal.PortalKey, - }, - GetDataFunc: func(ctx context.Context, portal *bridgev2.Portal) (*bridgev2.FetchMessagesResponse, error) { - if len(conv.GetMessages()) == 0 { - return &bridgev2.FetchMessagesResponse{}, nil - } - messages := make([]*waWeb.WebMessageInfo, len(conv.GetMessages())) - for i, rawMsg := range conv.GetMessages() { - messages[i] = rawMsg.Message - } - zerolog.Ctx(ctx).Debug(). - Int("message_count", len(messages)). - Stringer("end_of_history_type", conv.GetEndOfHistoryTransferType()). - Msg("Converting messages to bridge from on-demand history sync") - resp, err := wa.convertHistorySyncMessages(ctx, portal, portalJID, messages, false) - if err != nil { - return nil, err - } - resp.HasMore = conv.GetEndOfHistoryTransferType() == waHistorySync.Conversation_COMPLETE_BUT_MORE_MESSAGES_REMAIN_ON_PRIMARY - return resp, nil - }, - }) - } -} - func (wa *WhatsAppClient) convertHistorySyncMessage( ctx context.Context, portal *bridgev2.Portal, info *types.MessageInfo, msg, rawMsg *waE2E.Message, isViewOnce bool, reactions []*waWeb.Reaction, ) (*bridgev2.BackfillMessage, *wadb.MediaRequest) { @@ -784,12 +543,11 @@ func (wa *WhatsAppClient) convertHistorySyncMessage( } // TODO use proper intent intent := wa.Main.Bridge.Bot - msgID := waid.MakeMessageIDWithAltSender(info.Chat, info.Sender, info.SenderAlt, info.ID) wrapped := &bridgev2.BackfillMessage{ ConvertedMessage: wa.Main.MsgConv.ToMatrix(ctx, portal, wa.Client, intent, msg, rawMsg, info, isViewOnce, true, nil), - Sender: wa.makeEventSender(ctx, pickLID(info.Sender, info.SenderAlt)), - ID: msgID, - TxnID: networkid.TransactionID(msgID), + Sender: wa.makeEventSender(ctx, info.Sender), + ID: waid.MakeMessageID(info.Chat, info.Sender, info.ID), + TxnID: networkid.TransactionID(waid.MakeMessageID(info.Chat, info.Sender, info.ID)), Timestamp: info.Timestamp, StreamOrder: info.Timestamp.Unix(), Reactions: make([]*bridgev2.BackfillReaction, 0, len(reactions)), @@ -798,10 +556,10 @@ func (wa *WhatsAppClient) convertHistorySyncMessage( for _, reaction := range reactions { var sender types.JID if reaction.GetKey().GetFromMe() { - sender = wa.GetLID() + sender = wa.JID } else if reaction.GetKey().GetParticipant() != "" { sender, _ = types.ParseJID(*reaction.Key.Participant) - } else if info.Chat.Server == types.DefaultUserServer || info.Chat.Server == types.HiddenUserServer || info.Chat.Server == types.BotServer { + } else if info.Chat.Server == types.DefaultUserServer || info.Chat.Server == types.BotServer { sender = info.Chat } if sender.IsEmpty() { diff --git a/pkg/connector/capabilities.go b/pkg/connector/capabilities.go index 152e7ad..202569b 100644 --- a/pkg/connector/capabilities.go +++ b/pkg/connector/capabilities.go @@ -19,7 +19,6 @@ var WhatsAppGeneralCaps = &bridgev2.NetworkGeneralCapabilities{ AggressiveUpdateInfo: true, ImplicitReadReceipts: true, Provisioning: bridgev2.ProvisioningCapabilities{ - ImagePackImport: true, ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{ CreateDM: true, LookupPhone: true, @@ -52,7 +51,7 @@ func (wa *WhatsAppConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilit } func (wa *WhatsAppConnector) GetBridgeInfoVersion() (info, caps int) { - return 1, 9 + return 1, 7 } const WAMaxFileSize = 2000 * 1024 * 1024 @@ -67,7 +66,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel { } func capID() string { - base := "fi.mau.whatsapp.capabilities.2026_07_22" + base := "fi.mau.whatsapp.capabilities.2025_12_15" if ffmpeg.Supported() { return base + "+ffmpeg" } @@ -126,10 +125,10 @@ var whatsappCaps = &event.RoomFeatures{ event.CapMsgSticker: { MimeTypes: map[string]event.CapabilitySupportLevel{ "image/webp": event.CapLevelFullySupported, + // TODO see if sending lottie is possible + //"video/lottie+json": event.CapLevelFullySupported, "image/png": event.CapLevelPartialSupport, "image/jpeg": event.CapLevelPartialSupport, - // This will only be accepted if it was imported from WhatsApp - "video/lottie+json": event.CapLevelPartialSupport, }, Caption: event.CapLevelDropped, MaxSize: WAMaxFileSize, @@ -174,26 +173,21 @@ var whatsappCaps = &event.RoomFeatures{ event.MemberActionKick: event.CapLevelFullySupported, event.MemberActionLeave: event.CapLevelFullySupported, }, - MaxTextLength: MaxTextLength, - LocationMessage: event.CapLevelFullySupported, - Poll: event.CapLevelFullySupported, - PollEnd: event.CapLevelUnsupported, - PollHiddenVotes: event.CapLevelUnsupported, - PollDuplicateOptions: event.CapLevelUnsupported, - PollMaxOptions: 12, - PollOptionMaxLength: 100, - Reply: event.CapLevelFullySupported, - Edit: event.CapLevelFullySupported, - EditMaxAge: ptr.Ptr(jsontime.S(EditMaxAge)), - Delete: event.CapLevelFullySupported, - DeleteForMe: false, - DeleteMaxAge: ptr.Ptr(jsontime.S(2 * 24 * time.Hour)), - Reaction: event.CapLevelFullySupported, - ReactionCount: 1, - ReadReceipts: true, - TypingNotifications: true, - DisappearingTimer: waDisappearingCap, - DeleteChat: true, + MaxTextLength: MaxTextLength, + LocationMessage: event.CapLevelFullySupported, + Poll: event.CapLevelFullySupported, + Reply: event.CapLevelFullySupported, + Edit: event.CapLevelFullySupported, + EditMaxAge: ptr.Ptr(jsontime.S(EditMaxAge)), + Delete: event.CapLevelFullySupported, + DeleteForMe: false, + DeleteMaxAge: ptr.Ptr(jsontime.S(2 * 24 * time.Hour)), + Reaction: event.CapLevelFullySupported, + ReactionCount: 1, + ReadReceipts: true, + TypingNotifications: true, + DisappearingTimer: waDisappearingCap, + DeleteChat: true, } var whatsappDMCaps *event.RoomFeatures diff --git a/pkg/connector/chatinfo.go b/pkg/connector/chatinfo.go index 1461561..5555566 100644 --- a/pkg/connector/chatinfo.go +++ b/pkg/connector/chatinfo.go @@ -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") return } - if !chat.Found { - chatID, err = wa.GetStore().GetAltJID(ctx, chatID) - if err != nil { - zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get alternate JID to get chat settings") - return - } - chat, err = wa.GetStore().ChatSettings.GetChatSettings(ctx, chatID) - if err != nil { - zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get chat settings with alternate JID") - return - } - } info.UserLocal = &bridgev2.UserLocalPortalInfo{ MutedUntil: ptr.Ptr(chat.MutedUntil), } @@ -153,20 +141,15 @@ const PrivateChatTopic = "WhatsApp private chat" const BotChatTopic = "WhatsApp chat with a bot" func (wa *WhatsAppClient) wrapDMInfo(ctx context.Context, jid types.JID) *bridgev2.ChatInfo { - ownID := wa.JID - if jid.Server == types.HiddenUserServer { - ownID = wa.GetLID() - } info := &bridgev2.ChatInfo{ - Type: ptr.Ptr(database.RoomTypeDM), Topic: ptr.Ptr(PrivateChatTopic), Members: &bridgev2.ChatMemberList{ IsFull: true, TotalMemberCount: 2, OtherUserID: waid.MakeUserID(jid), MemberMap: map[networkid.UserID]bridgev2.ChatMember{ - waid.MakeUserID(jid): {EventSender: wa.makeEventSender(ctx, jid)}, - waid.MakeUserID(ownID): {EventSender: wa.makeEventSender(ctx, ownID)}, + waid.MakeUserID(jid): {EventSender: wa.makeEventSender(ctx, jid)}, + waid.MakeUserID(wa.JID): {EventSender: wa.makeEventSender(ctx, wa.JID)}, }, PowerLevels: &bridgev2.PowerLevelOverrides{ Events: map[event.Type]int{ @@ -176,14 +159,13 @@ func (wa *WhatsAppClient) wrapDMInfo(ctx context.Context, jid types.JID) *bridge event.StateBeeperDisappearingTimer: 0, }, }, - ExcludeChangesFromTimeline: true, }, - ExcludeChangesFromTimeline: true, + Type: ptr.Ptr(database.RoomTypeDM), } if jid.Server == types.BotServer { info.Topic = ptr.Ptr(BotChatTopic) } - if wa.IsOwnJID(jid) { + if jid == wa.JID.ToNonAD() { // For chats with self, force-split the members so the user's own ghost is always in the room. info.Members.MemberMap = map[networkid.UserID]bridgev2.ChatMember{ waid.MakeUserID(jid): {EventSender: bridgev2.EventSender{Sender: waid.MakeUserID(jid)}}, @@ -207,7 +189,7 @@ func (wa *WhatsAppClient) wrapStatusBroadcastInfo(ctx context.Context) *bridgev2 Members: &bridgev2.ChatMemberList{ IsFull: false, MemberMap: map[networkid.UserID]bridgev2.ChatMember{ - waid.MakeUserID(wa.GetLID()): {EventSender: wa.makeEventSender(ctx, wa.GetLID())}, + waid.MakeUserID(wa.JID): {EventSender: wa.makeEventSender(ctx, wa.JID)}, }, }, Type: ptr.Ptr(database.RoomTypeDefault), @@ -275,7 +257,7 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn Name: ptr.Ptr(info.Name), Topic: ptr.Ptr(info.Topic), Members: &bridgev2.ChatMemberList{ - IsFull: !info.IsIncognito && !info.IsParent, + IsFull: !info.IsIncognito, TotalMemberCount: len(info.Participants), MemberMap: make(map[networkid.UserID]bridgev2.ChatMember, len(info.Participants)), PowerLevels: &bridgev2.PowerLevelOverrides{ @@ -284,13 +266,14 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn Ban: ptr.Ptr(nobodyPL), // TODO allow invites if bridge config says to allow them, or maybe if relay mode is enabled? Events: map[event.Type]int{ - event.StateRoomName: metaChangePL, - event.StateRoomAvatar: metaChangePL, - event.StateTopic: metaChangePL, - event.EventReaction: defaultPL, - event.EventRedaction: defaultPL, - event.EventUnstablePollResponse: defaultPL, + event.StateRoomName: metaChangePL, + event.StateRoomAvatar: metaChangePL, + event.StateTopic: metaChangePL, + event.EventReaction: defaultPL, + event.EventRedaction: defaultPL, + event.StateBeeperDisappearingTimer: metaChangePL, + // TODO always allow poll responses }, }, }, @@ -301,15 +284,11 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn }, ExtraUpdates: extraUpdater, } - var hasSelf bool for _, pcp := range info.Participants { member := bridgev2.ChatMember{ EventSender: wa.makeEventSender(ctx, pcp.JID), Membership: event.MembershipJoin, } - if member.EventSender.IsFromMe { - hasSelf = true - } if pcp.IsSuperAdmin { member.PowerLevel = ptr.Ptr(superAdminPL) } else if pcp.IsAdmin { @@ -320,21 +299,18 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn member.MemberEventExtra = map[string]any{ "com.beeper.exclude_from_timeline": true, } - wrapped.Members.MemberMap.Set(member) + wrapped.Members.MemberMap[waid.MakeUserID(pcp.JID)] = member if pcp.JID.Server == types.HiddenUserServer && !pcp.PhoneNumber.IsEmpty() { - wrapped.Members.MemberMap.Add(bridgev2.ChatMember{ + wrapped.Members.MemberMap[waid.MakeUserID(pcp.PhoneNumber)] = bridgev2.ChatMember{ EventSender: bridgev2.EventSender{Sender: waid.MakeUserID(pcp.PhoneNumber)}, Membership: event.MembershipLeave, PrevMembership: event.MembershipJoin, MemberEventExtra: map[string]any{ "com.beeper.exclude_from_timeline": true, }, - }) + } } } - if info.IsParent && !hasSelf && info.AddressingMode == types.AddressingModeLID { - wrapped.Members.MemberMap.Add(bridgev2.ChatMember{EventSender: wa.makeEventSender(ctx, wa.GetLID())}) - } if !info.LinkedParentJID.IsEmpty() { wrapped.ParentID = ptr.Ptr(waid.MakePortalID(info.LinkedParentJID)) @@ -474,7 +450,7 @@ func (wa *WhatsAppClient) makePortalAvatarFetcher(avatarID string, sender types. wrappedAvatar = &bridgev2.Avatar{ ID: networkid.AvatarID(avatar.ID), Get: func(ctx context.Context) ([]byte, error) { - return wa.Client.DownloadMediaWithOnlyPath(ctx, avatar.DirectPath) + return wa.Client.DownloadMediaWithPath(ctx, avatar.DirectPath, nil, nil, nil, 0, "", "") }, } } @@ -513,7 +489,7 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne if info.ThreadMeta.Picture != nil { avatar.ID = networkid.AvatarID(info.ThreadMeta.Picture.ID) avatar.Get = func(ctx context.Context) ([]byte, error) { - return wa.Client.DownloadMediaWithOnlyPath(ctx, info.ThreadMeta.Picture.DirectPath) + return wa.Client.DownloadMediaWithPath(ctx, info.ThreadMeta.Picture.DirectPath, nil, nil, nil, 0, "", "") } } else if info.ThreadMeta.Preview.ID != "" { avatar.ID = networkid.AvatarID(info.ThreadMeta.Preview.ID) @@ -524,7 +500,7 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne } else if meta.ThreadMeta.Picture == nil { return nil, fmt.Errorf("full res avatar info is missing") } - return wa.Client.DownloadMediaWithOnlyPath(ctx, meta.ThreadMeta.Picture.DirectPath) + return wa.Client.DownloadMediaWithPath(ctx, meta.ThreadMeta.Picture.DirectPath, nil, nil, nil, 0, "", "") } } else { avatar.ID = "remove" @@ -540,8 +516,8 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne Members: &bridgev2.ChatMemberList{ TotalMemberCount: info.ThreadMeta.SubscriberCount, MemberMap: map[networkid.UserID]bridgev2.ChatMember{ - waid.MakeUserID(wa.GetLID()): { - EventSender: wa.makeEventSender(ctx, wa.GetLID()), + waid.MakeUserID(wa.JID): { + EventSender: wa.makeEventSender(ctx, wa.JID), PowerLevel: &ownPowerLevel, }, }, @@ -550,12 +526,12 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne StateDefault: ptr.Ptr(nobodyPL), Ban: ptr.Ptr(nobodyPL), Events: map[event.Type]int{ - event.StateRoomName: adminPL, - event.StateRoomAvatar: adminPL, - event.StateTopic: adminPL, - event.EventReaction: defaultPL, - event.EventRedaction: defaultPL, - event.EventUnstablePollResponse: defaultPL, + event.StateRoomName: adminPL, + event.StateRoomAvatar: adminPL, + event.StateTopic: adminPL, + event.EventReaction: defaultPL, + event.EventRedaction: defaultPL, + // TODO always allow poll responses }, }, }, diff --git a/pkg/connector/client.go b/pkg/connector/client.go index b3ecdba..60ef563 100644 --- a/pkg/connector/client.go +++ b/pkg/connector/client.go @@ -29,6 +29,7 @@ import ( "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/appstate" waBinary "go.mau.fi/whatsmeow/binary" + "go.mau.fi/whatsmeow/proto/waHistorySync" "go.mau.fi/whatsmeow/proto/waWa6" "go.mau.fi/whatsmeow/store" "go.mau.fi/whatsmeow/types" @@ -38,7 +39,6 @@ import ( "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/bridgev2/status" - "maunium.net/go/mautrix/event" "go.mau.fi/mautrix-whatsapp/pkg/waid" ) @@ -49,14 +49,14 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2. UserLogin: login, MC: noopMCInstance, + historySyncs: make(chan *waHistorySync.HistorySync, 64), historySyncWakeup: make(chan struct{}, 1), resyncQueue: make(map[types.JID]resyncQueueItem), directMediaRetries: make(map[networkid.MessageID]*directMediaRetry), mediaRetryLock: semaphore.NewWeighted(wa.Config.HistorySync.MediaRequests.MaxAsyncHandle), pushNamesSynced: exsync.NewEvent(), + createDedup: exsync.NewSet[types.MessageID](), appStateFullSyncAttempted: make(map[appstate.WAPatchName]time.Time), - - disableNewsletter: store.BaseClientPayload.GetUserAgent().GetPlatform() == waWa6.ClientPayload_UserAgent_MACOS, } login.Client = w @@ -71,7 +71,6 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2. if err != nil { return err } - w.LID = w.Device.GetLID() if w.Device != nil { log := w.UserLogin.Log.With().Str("component", "whatsmeow").Logger() @@ -106,9 +105,9 @@ type WhatsAppClient struct { Client *whatsmeow.Client Device *store.Device JID types.JID - LID types.JID MC mClient + historySyncs chan *waHistorySync.HistorySync historySyncWakeup chan struct{} stopLoops atomic.Pointer[context.CancelFunc] resyncQueue map[types.JID]resyncQueueItem @@ -121,8 +120,7 @@ type WhatsAppClient struct { isNewLogin bool pushNamesSynced *exsync.Event lastPresence types.Presence - - disableNewsletter bool + createDedup *exsync.Set[types.MessageID] appStateRecoveryLock sync.Mutex appStateFullSyncAttempted map[appstate.WAPatchName]time.Time @@ -133,7 +131,6 @@ var ( _ bridgev2.PushableNetworkAPI = (*WhatsAppClient)(nil) _ bridgev2.BackgroundSyncingNetworkAPI = (*WhatsAppClient)(nil) _ bridgev2.ChatViewingNetworkAPI = (*WhatsAppClient)(nil) - _ bridgev2.StickerImportingNetworkAPI = (*WhatsAppClient)(nil) ) var pushCfg = &bridgev2.PushConfig{ @@ -189,19 +186,7 @@ func (wa *WhatsAppClient) RegisterPushNotifications(ctx context.Context, pushTyp } func (wa *WhatsAppClient) IsThisUser(_ context.Context, userID networkid.UserID) bool { - return userID == waid.MakeUserID(wa.JID) || userID == waid.MakeUserID(wa.GetLID()) -} - -func (wa *WhatsAppClient) IsOwnJID(jid types.JID) bool { - return (jid.Server == types.DefaultUserServer && jid.User == wa.JID.User) || - (jid.Server == types.HiddenUserServer && jid.User == wa.GetLID().User) -} - -func (wa *WhatsAppClient) GetLID() types.JID { - if wa.LID.IsEmpty() && !wa.JID.IsEmpty() { - wa.LID = wa.GetStore().GetLID() - } - return wa.LID + return userID == waid.MakeUserID(wa.JID) } func (wa *WhatsAppClient) Connect(ctx context.Context) { @@ -226,7 +211,6 @@ func (wa *WhatsAppClient) Connect(ctx context.Context) { wa.Client.BackgroundEventCtx = wa.UserLogin.Log.WithContext(wa.Main.Bridge.BackgroundCtx) zerolog.Ctx(ctx).Debug().Msg("Connecting to WhatsApp") if err := wa.Client.ConnectContext(ctx); err != nil { - wa.callStopLoops() zerolog.Ctx(ctx).Err(err).Msg("Failed to connect to WhatsApp") state := status.BridgeState{ StateEvent: status.StateUnknownError, @@ -282,9 +266,7 @@ func (wa *WhatsAppClient) ConnectBackground(ctx context.Context, params *bridgev return payload } defer func() { - if cli := wa.Client; cli != nil { - cli.GetClientPayload = nil - } + wa.Client.GetClientPayload = nil }() err := wa.Client.ConnectContext(ctx) if err != nil { @@ -353,7 +335,6 @@ func (wa *WhatsAppClient) startLoops() { if oldStop != nil { (*oldStop)() } - ctx = wa.UserLogin.Log.WithContext(ctx) go wa.historySyncLoop(ctx) go wa.ghostResyncLoop(ctx) if mrc := wa.Main.Config.HistorySync.MediaRequests; mrc.AutoRequestMedia && mrc.RequestMethod == MediaRequestMethodLocalTime { @@ -371,14 +352,10 @@ func (wa *WhatsAppClient) GetStore() *store.Device { return store.NoopDevice } -func (wa *WhatsAppClient) callStopLoops() { +func (wa *WhatsAppClient) Disconnect() { if stopHistorySyncLoop := wa.stopLoops.Swap(nil); stopHistorySyncLoop != nil { (*stopHistorySyncLoop)() } -} - -func (wa *WhatsAppClient) Disconnect() { - wa.callStopLoops() if cli := wa.Client; cli != nil { cli.Disconnect() } @@ -485,12 +462,3 @@ func (wa *WhatsAppClient) updatePresence(ctx context.Context, presence types.Pre } return err } - -func (wa *WhatsAppClient) DownloadImagePack(ctx context.Context, url string) (*bridgev2.ImportedImagePack, error) { - return wa.Main.MsgConv.DownloadImagePack(ctx, wa.UserLogin.ID, wa.Client, url) -} - -func (wa *WhatsAppClient) ListImagePacks(ctx context.Context) ([]*event.ImagePackMetadata, error) { - // TODO - return nil, nil -} diff --git a/pkg/connector/config.go b/pkg/connector/config.go index e51c80a..6a097c2 100644 --- a/pkg/connector/config.go +++ b/pkg/connector/config.go @@ -2,7 +2,6 @@ package connector import ( _ "embed" - "fmt" "strings" "text/template" "time" @@ -71,8 +70,6 @@ type Config struct { RequestLocalTime int `yaml:"request_local_time"` MaxAsyncHandle int64 `yaml:"max_async_handle"` } `yaml:"media_requests"` - - BackwardsOnDemand bool `yaml:"backwards_on_demand"` } `yaml:"history_sync"` displaynameTemplate *template.Template `yaml:"-"` @@ -91,15 +88,7 @@ func (c *Config) UnmarshalYAML(node *yaml.Node) error { func (c *Config) PostProcess() error { var err error c.displaynameTemplate, err = template.New("displayname").Parse(c.DisplaynameTemplate) - if err != nil { - return err - } - // Try to execute template to make sure it's valid - _, err = c.formatDisplayname(types.PSAJID, "", types.ContactInfo{}) - if err != nil { - return fmt.Errorf("failed to execute displayname template: %w", err) - } - return nil + return err } func upgradeConfig(helper up.Helper) { @@ -145,7 +134,6 @@ func upgradeConfig(helper up.Helper) { helper.Copy(up.Str, "history_sync", "media_requests", "request_method") helper.Copy(up.Int, "history_sync", "media_requests", "request_local_time") helper.Copy(up.Int, "history_sync", "media_requests", "max_async_handle") - helper.Copy(up.Bool, "history_sync", "backwards_on_demand") } type DisplaynameParams struct { @@ -160,7 +148,7 @@ type DisplaynameParams struct { Short string } -func (c *Config) formatDisplayname(jid types.JID, phone string, contact types.ContactInfo) (string, error) { +func (c *Config) FormatDisplayname(jid types.JID, phone string, contact types.ContactInfo) string { var nameBuf strings.Builder if phone == "" && jid.Server == types.DefaultUserServer { phone = "+" + jid.User @@ -179,21 +167,13 @@ func (c *Config) formatDisplayname(jid types.JID, phone string, contact types.Co Name: contact.FullName, Short: contact.FirstName, }) - return nameBuf.String(), err -} - -func (c *Config) FormatDisplayname(jid types.JID, phone string, contact types.ContactInfo) string { - name, err := c.formatDisplayname(jid, phone, contact) if err != nil { panic(err) } - return name + return nameBuf.String() } func redactPhone(phone string) string { - if len(phone) <= 4 { - return phone - } // This doesn't keep 2+ digit country codes properly, but whatever return phone[:2] + strings.Repeat("∙", len(phone)-4) + phone[len(phone)-2:] } diff --git a/pkg/connector/connector.go b/pkg/connector/connector.go index e262c82..91b1e11 100644 --- a/pkg/connector/connector.go +++ b/pkg/connector/connector.go @@ -28,24 +28,27 @@ import ( "time" "github.com/lib/pq" + "github.com/rs/zerolog" "go.mau.fi/util/dbutil" - "go.mau.fi/util/exsync" "go.mau.fi/util/random" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/proto/waCompanionReg" "go.mau.fi/whatsmeow/store" "go.mau.fi/whatsmeow/store/sqlstore" whatsmeowUpgrades "go.mau.fi/whatsmeow/store/sqlstore/upgrades" + "go.mau.fi/whatsmeow/types" waLog "go.mau.fi/whatsmeow/util/log" "google.golang.org/protobuf/proto" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/commands" + "maunium.net/go/mautrix/bridgev2/database" "maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/event" "maunium.net/go/mautrix/id" "go.mau.fi/mautrix-whatsapp/pkg/connector/wadb" "go.mau.fi/mautrix-whatsapp/pkg/msgconv" + "go.mau.fi/mautrix-whatsapp/pkg/waid" ) type WhatsAppConnector struct { @@ -61,8 +64,6 @@ type WhatsAppConnector struct { mediaEditCache MediaEditCache mediaEditCacheLock sync.RWMutex stopMediaEditCacheLoop atomic.Pointer[context.CancelFunc] - - unmigratedDMs *exsync.Set[networkid.PortalKey] } func init() { @@ -113,7 +114,6 @@ func (wa *WhatsAppConnector) Init(bridge *bridgev2.Bridge) { cmdAccept, cmdSync, cmdInviteLink, cmdResolveLink, cmdJoin, ) wa.mediaEditCache = make(MediaEditCache) - wa.unmigratedDMs = exsync.NewSet[networkid.PortalKey]() whatsmeowDBLog := bridge.Log.With().Str("db_section", "whatsmeow").Logger() wa.DeviceStore = sqlstore.NewWithWrappedDB( @@ -157,7 +157,78 @@ func (wa *WhatsAppConnector) Start(ctx context.Context) error { 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() { diff --git a/pkg/connector/directmedia.go b/pkg/connector/directmedia.go index 8a7afd9..a4a3d73 100644 --- a/pkg/connector/directmedia.go +++ b/pkg/connector/directmedia.go @@ -17,7 +17,6 @@ package connector import ( - "bytes" "context" "encoding/json" "errors" @@ -52,7 +51,7 @@ func (wa *WhatsAppConnector) SetUseDirectMedia() { } var ErrReloadNeeded = mautrix.RespError{ - ErrCode: "COM.BEEPER.MEDIA_RELOAD_NEEDED", + ErrCode: "FI.MAU.WHATSAPP_RELOAD_NEEDED", Err: "Media is no longer available on WhatsApp servers and must be re-requested from your phone", StatusCode: http.StatusNotFound, } @@ -68,8 +67,6 @@ func (wa *WhatsAppConnector) Download(ctx context.Context, mediaID networkid.Med return wa.downloadMessageDirectMedia(ctx, parsedID, params) } else if parsedID.Avatar != nil { return wa.downloadAvatarDirectMedia(ctx, parsedID, params) - } else if parsedID.Sticker != nil { - return wa.downloadStickerDirectMedia(ctx, parsedID, params) } else { return nil, fmt.Errorf("unexpected media ID parsing result") } @@ -131,30 +128,15 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars } return &mediaproxy.GetMediaResponseFile{ Callback: func(w *os.File) (*mediaproxy.FileMeta, error) { - return &mediaproxy.FileMeta{}, waClient.Client.DownloadMediaWithOnlyPathToFile(ctx, cachedInfo.DirectPath, w) + return &mediaproxy.FileMeta{}, waClient.Client.DownloadMediaWithPathToFile( + ctx, cachedInfo.DirectPath, nil, nil, nil, 0, "", "", w, + ) }, }, nil } -func (wa *WhatsAppConnector) downloadStickerDirectMedia(ctx context.Context, parsedID *waid.ParsedMediaID, params map[string]string) (mediaproxy.GetMediaResponse, error) { - ul := wa.Bridge.GetCachedUserLoginByID(parsedID.UserLogin) - if ul == nil { - return nil, fmt.Errorf("%w: user login %s not found", bridgev2.ErrNotLoggedIn, parsedID.UserLogin) - } - waClient := ul.Client.(*WhatsAppClient) - if waClient.Client == nil { - return nil, fmt.Errorf("no WhatsApp client found on login %s", parsedID.UserLogin) - } - sticker, err := wa.MsgConv.GetCachedSticker(ctx, waClient.Client, parsedID.Sticker.PackID, parsedID.Sticker.FileHash) - if err != nil { - return nil, err - } else if sticker == nil { - return nil, mautrix.MNotFound.WithMessage("Sticker not found in pack") - } - return wa.makeDirectMediaResponse(ctx, waClient, sticker, sticker.MimeType, "", nil, params) -} - func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, parsedID *waid.ParsedMediaID, params map[string]string) (mediaproxy.GetMediaResponse, error) { + log := zerolog.Ctx(ctx) msg, err := wa.Bridge.DB.Message.GetFirstPartByID(ctx, parsedID.UserLogin, parsedID.Message.String()) if err != nil { return nil, fmt.Errorf("failed to get message: %w", err) @@ -170,9 +152,6 @@ func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, par if err != nil { return nil, fmt.Errorf("failed to unmarshal media keys: %w", err) } - if version := parsedID.Message.Version; len(version) > 0 && !bytes.Equal(version, keys.EncSHA256) && !bytes.Equal(version, keys.SHA256) { - return nil, mautrix.MNotFound.WithMessage("Version mismatch, media was likely replaced") - } var ul *bridgev2.UserLogin if parsedID.UserLogin != "" { ul = wa.Bridge.GetCachedUserLoginByID(parsedID.UserLogin) @@ -195,29 +174,16 @@ func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, par if waClient.Client == nil { return nil, fmt.Errorf("no WhatsApp client found on login") } - return wa.makeDirectMediaResponse(ctx, waClient, keys, keys.MimeType, msg.ID, keys, params) -} - -func (wa *WhatsAppConnector) makeDirectMediaResponse( - ctx context.Context, - waClient *WhatsAppClient, - dm whatsmeow.DownloadableMessage, - mimeType string, - msgID networkid.MessageID, - keys *msgconv.FailedMediaKeys, - params map[string]string, -) (mediaproxy.GetMediaResponse, error) { return &mediaproxy.GetMediaResponseFile{ Callback: func(f *os.File) (*mediaproxy.FileMeta, error) { - log := zerolog.Ctx(ctx) - err := waClient.Client.DownloadToFile(ctx, dm, f) - if keys != nil && (errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith403) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith410) || errors.Is(err, whatsmeow.ErrNoURLPresent)) { - val := params["com.beeper.interactive_download_request"] + err := waClient.Client.DownloadToFile(ctx, keys, f) + if errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith403) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith410) || errors.Is(err, whatsmeow.ErrNoURLPresent) { + val := params["fi.mau.whatsapp.reload_media"] if val == "false" || (!wa.Config.DirectMediaAutoRequest && val != "true") { return nil, ErrReloadNeeded } log.Trace().Msg("Media not found for direct download, requesting and waiting") - err = waClient.requestAndWaitDirectMedia(ctx, msgID, keys) + err = waClient.requestAndWaitDirectMedia(ctx, msg.ID, keys) if err != nil { log.Trace().Err(err).Msg("Failed to wait for media for direct download") return nil, err @@ -225,29 +191,30 @@ func (wa *WhatsAppConnector) makeDirectMediaResponse( log.Trace().Msg("Retrying download after successful retry") err = waClient.Client.DownloadToFile(ctx, keys, f) } - if errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) { + if errors.Is(err, whatsmeow.ErrFileLengthMismatch) || errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) { zerolog.Ctx(ctx).Warn().Err(err).Msg("Mismatching media checksums in message. Ignoring because WhatsApp seems to ignore them too") } else if err != nil { return nil, err } - if mimeType == "application/was" { + mime := keys.MimeType + if mime == "application/was" { if _, err := f.Seek(0, io.SeekStart); err != nil { return nil, fmt.Errorf("failed to seek to start of sticker zip: %w", err) } else if zipData, err := io.ReadAll(f); err != nil { return nil, fmt.Errorf("failed to read sticker zip: %w", err) - } else if data, _, err := msgconv.ExtractAnimatedSticker(zipData); err != nil { + } else if data, err := msgconv.ExtractAnimatedSticker(zipData); err != nil { return nil, fmt.Errorf("failed to extract animated sticker: %w %x", err, zipData) } else if _, err := f.WriteAt(data, 0); err != nil { return nil, fmt.Errorf("failed to write animated sticker to file: %w", err) } else if err := f.Truncate(int64(len(data))); err != nil { return nil, fmt.Errorf("failed to truncate animated sticker file: %w", err) } - mimeType = "video/lottie+json" + mime = "video/lottie+json" } return &mediaproxy.FileMeta{ - ContentType: mimeType, + ContentType: mime, }, nil }, }, nil diff --git a/pkg/connector/events.go b/pkg/connector/events.go index 1f4e118..3fd63dd 100644 --- a/pkg/connector/events.go +++ b/pkg/connector/events.go @@ -39,6 +39,18 @@ import ( "go.mau.fi/mautrix-whatsapp/pkg/waid" ) +func (wa *WhatsAppClient) getPortalKeyByMessageSource(ms types.MessageSource) networkid.PortalKey { + jid := ms.Chat + if ms.IsIncomingBroadcast() { + if ms.IsFromMe { + jid = ms.BroadcastListOwner.ToNonAD() + } else { + jid = ms.Sender.ToNonAD() + } + } + return wa.makeWAPortalKey(jid) +} + type MessageInfoWrapper struct { Info types.MessageInfo wa *WhatsAppClient @@ -49,26 +61,7 @@ func (evt *MessageInfoWrapper) ShouldCreatePortal() bool { } func (evt *MessageInfoWrapper) GetPortalKey() networkid.PortalKey { - ms := evt.Info.MessageSource - jid := ms.Chat - if ms.IsIncomingBroadcast() { - if ms.IsFromMe { - // TODO can this still be a phone number? - jid = ms.BroadcastListOwner.ToNonAD() - } else { - jid = ms.Sender.ToNonAD() - if jid.Server == types.DefaultUserServer && !ms.SenderAlt.IsEmpty() { - jid = ms.SenderAlt.ToNonAD() - } - } - } else if jid.Server == types.DefaultUserServer { - if !ms.IsFromMe && ms.Chat.ToNonAD() == ms.Sender.ToNonAD() && !ms.SenderAlt.IsEmpty() { - jid = ms.SenderAlt.ToNonAD() - } else if !ms.RecipientAlt.IsEmpty() { - jid = ms.RecipientAlt.ToNonAD() - } - } - return evt.wa.makeWAPortalKey(jid) + return evt.wa.getPortalKeyByMessageSource(evt.Info.MessageSource) } func (evt *MessageInfoWrapper) AddLogContext(c zerolog.Context) zerolog.Context { @@ -79,19 +72,12 @@ func (evt *MessageInfoWrapper) GetTimestamp() time.Time { return evt.Info.Timestamp } -func pickLID(main, alt types.JID) types.JID { - if main.Server == types.DefaultUserServer && alt.Server == types.HiddenUserServer { - return alt - } - return main -} - func (evt *MessageInfoWrapper) GetSender() bridgev2.EventSender { - return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, pickLID(evt.Info.Sender, evt.Info.SenderAlt)) + return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, evt.Info.Sender) } func (evt *MessageInfoWrapper) GetID() networkid.MessageID { - return waid.MakeMessageIDWithAltSender(evt.Info.Chat, evt.Info.Sender, evt.Info.SenderAlt, evt.Info.ID) + return waid.MakeMessageID(evt.Info.Chat, evt.Info.Sender, evt.Info.ID) } func (evt *MessageInfoWrapper) GetTransactionID() networkid.TransactionID { @@ -105,7 +91,6 @@ type WAMessageEvent struct { parsedMessageType string isUndecryptableUpsertSubEvent bool - dontRenderEdited bool postHandle func() } @@ -148,6 +133,14 @@ func (evt *WAMessageEvent) PreHandle(ctx context.Context, portal *bridgev2.Porta return } meta := portal.Metadata.(*waid.PortalMetadata) + if meta.AddressingMode == types.AddressingModeLID && evt.Info.Sender.Server == types.DefaultUserServer { + evt.Info.Sender, evt.Info.SenderAlt = evt.Info.SenderAlt, evt.Info.Sender + zerolog.Ctx(ctx).Debug(). + Stringer("lid", evt.Info.Sender). + Stringer("pn", evt.Info.SenderAlt). + Str("message_id", evt.Info.ID). + Msg("Forced phone number sender to LID in group message") + } if meta.AddressingMode == types.AddressingModeLID || meta.LIDMigrationAttempted { return } @@ -166,6 +159,13 @@ func (evt *WAMessageEvent) PreHandle(ctx context.Context, portal *bridgev2.Porta log.Info().Msg("Resyncing group members as it appears to have switched to LID addressing mode") portal.UpdateInfo(ctx, evt.wa.wrapGroupInfo(ctx, info), evt.wa.UserLogin, nil, time.Time{}) log.Debug().Msg("Finished resyncing after LID change") + if evt.Info.Sender.Server == types.DefaultUserServer && evt.Info.SenderAlt.Server == types.HiddenUserServer { + evt.Info.Sender, evt.Info.SenderAlt = evt.Info.SenderAlt, evt.Info.Sender + log.Debug(). + Stringer("new_sender", evt.Info.Sender). + Stringer("new_sender_alt", evt.Info.SenderAlt). + Msg("Overriding sender to LID after resyncing group members") + } } func (evt *WAMessageEvent) PostHandle(ctx context.Context, portal *bridgev2.Portal) { @@ -181,15 +181,12 @@ func (evt *WAMessageEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Por } var editedMsg *waE2E.Message var previouslyConvertedPart *bridgev2.ConvertedMessagePart - targetMessage := evt.GetTargetMessage() - cacheMessage := targetMessage if evt.isUndecryptableUpsertSubEvent { // TODO db metadata needs to be updated in this case to remove the error editedMsg = evt.Message - cacheMessage = evt.GetID() } else { editedMsg = evt.Message.GetProtocolMessage().GetEditedMessage() - previouslyConvertedPart = evt.wa.Main.GetMediaEditCache(portal, targetMessage) + previouslyConvertedPart = evt.wa.Main.GetMediaEditCache(portal, evt.GetTargetMessage()) meta := existing[0].Metadata.(*waid.MessageMetadata) if slices.Contains(meta.Edits, evt.Info.ID) { return nil, fmt.Errorf("%w: edit already handled", bridgev2.ErrIgnoringRemoteEvent) @@ -205,11 +202,9 @@ func (evt *WAMessageEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Por evt.postHandle = func() { evt.wa.processFailedMedia(ctx, portal.PortalKey, evt.GetID(), cm, false) } - } else if len(cm.Parts) > 0 && cacheMessage != "" { - evt.wa.Main.AddMediaEditCache(portal, cacheMessage, cm.Parts[0]) } editPart := cm.Parts[0].ToEditPart(existing[0]) - if evt.isUndecryptableUpsertSubEvent || evt.dontRenderEdited { + if evt.isUndecryptableUpsertSubEvent { if editPart.TopLevelExtra == nil { editPart.TopLevelExtra = make(map[string]any) } @@ -269,6 +264,18 @@ func (evt *WAMessageEvent) GetType() bridgev2.RemoteEventType { func (evt *WAMessageEvent) HandleExisting(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, existing []*database.Message) (bridgev2.UpsertResult, error) { if existing[0].Metadata.(*waid.MessageMetadata).Error == waid.MsgErrDecryptionFailed { evt.wa.trackUndecryptableResolved(evt.MsgEvent) + if existing[0].HasFakeMXID() { + // The undecryptable message was hidden (decrypt_fail=hide), so no Matrix + // event was sent. Delete the placeholder DB entry and let the framework + // handle this as a brand-new message so a real Matrix event is created. + zerolog.Ctx(ctx).Debug(). + Msg("Received decryptable version of previously hidden undecryptable message, re-handling as new message") + err := portal.Bridge.DB.Message.DeleteAllParts(ctx, portal.Receiver, evt.GetID()) + if err != nil { + return bridgev2.UpsertResult{}, fmt.Errorf("failed to delete hidden placeholder message: %w", err) + } + return bridgev2.UpsertResult{ContinueMessageHandling: true}, nil + } zerolog.Ctx(ctx).Debug(). Stringer("existing_mxid", existing[0].MXID). Msg("Received decryptable version of previously undecryptable message") @@ -324,7 +331,8 @@ func (evt *WANowDecryptableMessage) GetType() bridgev2.RemoteEventType { type WAUndecryptableMessage struct { *MessageInfoWrapper - Type events.UnavailableType + Type events.UnavailableType + Hidden bool } var ( @@ -371,9 +379,10 @@ func (evt *WAUndecryptableMessage) ConvertMessage(ctx context.Context, portal *b // TODO thread root for comments return &bridgev2.ConvertedMessage{ Parts: []*bridgev2.ConvertedMessagePart{{ - Type: event.EventMessage, - Content: content, - Extra: extra, + Type: event.EventMessage, + Content: content, + Extra: extra, + DontBridge: evt.Hidden, DBMetadata: &waid.MessageMetadata{ SenderDeviceID: evt.Info.Sender.Device, Error: waid.MsgErrDecryptionFailed, @@ -390,9 +399,7 @@ func (evt *WAUndecryptableMessage) GetStreamOrder() int64 { type WAMediaRetry struct { *events.MediaRetry - wa *WhatsAppClient - senderLID types.JID - chatLID types.JID + wa *WhatsAppClient } func (evt *WAMediaRetry) GetType() bridgev2.RemoteEventType { @@ -400,7 +407,7 @@ func (evt *WAMediaRetry) GetType() bridgev2.RemoteEventType { } func (evt *WAMediaRetry) GetPortalKey() networkid.PortalKey { - return evt.wa.makeWAPortalKey(pickLID(evt.ChatID, evt.chatLID)) + return evt.wa.makeWAPortalKey(evt.ChatID) } func (evt *WAMediaRetry) AddLogContext(c zerolog.Context) zerolog.Context { @@ -414,23 +421,16 @@ func (evt *WAMediaRetry) AddLogContext(c zerolog.Context) zerolog.Context { func (evt *WAMediaRetry) getRealSender() types.JID { sender := evt.SenderID - if sender.IsEmpty() { - if evt.FromMe { - if evt.ChatID.Server == types.HiddenUserServer { - sender = evt.wa.GetLID().ToNonAD() - } else { - sender = evt.wa.JID.ToNonAD() - } - } else if evt.ChatID.Server == types.DefaultUserServer || evt.ChatID.Server == types.HiddenUserServer || evt.ChatID.Server == types.BotServer { - sender = evt.ChatID.ToNonAD() - } + if evt.FromMe { + sender = evt.wa.JID.ToNonAD() + } else if sender.IsEmpty() && (evt.ChatID.Server == types.DefaultUserServer || evt.ChatID.Server == types.BotServer) { + sender = evt.ChatID.ToNonAD() } return sender } func (evt *WAMediaRetry) GetSender() bridgev2.EventSender { - realSender := pickLID(evt.getRealSender(), evt.senderLID) - return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, realSender) + return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, evt.getRealSender()) } func (evt *WAMediaRetry) GetTargetMessage() networkid.MessageID { diff --git a/pkg/connector/example-config.yaml b/pkg/connector/example-config.yaml index 564f25e..e441f67 100644 --- a/pkg/connector/example-config.yaml +++ b/pkg/connector/example-config.yaml @@ -121,6 +121,3 @@ history_sync: request_local_time: 120 # Maximum number of media request responses to handle in parallel per user. max_async_handle: 2 - # Use on-demand history sync requests for fetching older messages? - # This only applies when using the backfill queue, never for forward backfills. - backwards_on_demand: false diff --git a/pkg/connector/handlematrix.go b/pkg/connector/handlematrix.go index c55f523..a330a98 100644 --- a/pkg/connector/handlematrix.go +++ b/pkg/connector/handlematrix.go @@ -48,12 +48,11 @@ var ( _ bridgev2.DeleteChatHandlingNetworkAPI = (*WhatsAppClient)(nil) ) -func (wa *WhatsAppClient) HandleMatrixPollStart(ctx context.Context, msg *bridgev2.MatrixPollStart) (result *bridgev2.MatrixMessageResponse, retErr error) { +func (wa *WhatsAppClient) HandleMatrixPollStart(ctx context.Context, msg *bridgev2.MatrixPollStart) (*bridgev2.MatrixMessageResponse, error) { waMsg, optionMap, err := wa.Main.MsgConv.PollStartToWhatsApp(ctx, msg.Content, msg.ReplyTo, msg.Portal) if err != nil { return nil, fmt.Errorf("failed to convert poll vote: %w", err) } - defer wa.mcTrack(msg, time.Now(), &retErr) resp, err := wa.handleConvertedMatrixMessage(ctx, &msg.MatrixMessage, waMsg, nil) if err != nil { return nil, err @@ -68,21 +67,19 @@ func (wa *WhatsAppClient) HandleMatrixPollStart(ctx context.Context, msg *bridge return resp, nil } -func (wa *WhatsAppClient) HandleMatrixPollVote(ctx context.Context, msg *bridgev2.MatrixPollVote) (result *bridgev2.MatrixMessageResponse, retErr error) { +func (wa *WhatsAppClient) HandleMatrixPollVote(ctx context.Context, msg *bridgev2.MatrixPollVote) (*bridgev2.MatrixMessageResponse, error) { waMsg, err := wa.Main.MsgConv.PollVoteToWhatsApp(ctx, wa.Client, msg.Content, msg.VoteTo) if err != nil { return nil, fmt.Errorf("failed to convert poll vote: %w", err) } - defer wa.mcTrack(msg, time.Now(), &retErr) return wa.handleConvertedMatrixMessage(ctx, &msg.MatrixMessage, waMsg, nil) } -func (wa *WhatsAppClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (result *bridgev2.MatrixMessageResponse, retErr error) { +func (wa *WhatsAppClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (*bridgev2.MatrixMessageResponse, error) { waMsg, req, err := wa.Main.MsgConv.ToWhatsApp(ctx, wa.Client, msg.Event, msg.Content, msg.ReplyTo, msg.ThreadRoot, msg.Portal) if err != nil { return nil, fmt.Errorf("failed to convert message: %w", err) } - defer wa.mcTrack(msg, time.Now(), &retErr) return wa.handleConvertedMatrixMessage(ctx, msg, waMsg, req) } @@ -103,23 +100,19 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg if err != nil { return nil, err } - if chatJID.Server == types.DefaultUserServer { - zerolog.Ctx(ctx).Warn().Stringer("portal_jid", chatJID).Msg("Matrix message received in phone number portal") - } if chatJID == types.StatusBroadcastJID && wa.Main.Config.DisableStatusBroadcastSend { return nil, ErrBroadcastSendDisabled } wrappedMsgID := waid.MakeMessageID(chatJID, wa.JID, req.ID) - wrappedMsgID2 := waid.MakeMessageID(chatJID, wa.GetLID(), req.ID) + wrappedMsgID2 := waid.MakeMessageID(chatJID, wa.GetStore().GetLID(), req.ID) msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID)) msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID2)) - zerolog.Ctx(ctx).Trace().Any("payload", waMsg).Msg("Outgoing message payload") resp, err := wa.Client.SendMessage(ctx, chatJID, waMsg, *req) if err != nil { return nil, err } var pickedMessageID networkid.MessageID - if resp.Sender == wa.GetLID() { + if resp.Sender == wa.GetStore().GetLID() && chatJID.Server != types.DefaultUserServer { pickedMessageID = wrappedMsgID2 msg.RemovePending(networkid.TransactionID(wrappedMsgID)) } else { @@ -140,17 +133,18 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg }, nil } -func (wa *WhatsAppClient) PreHandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (bridgev2.MatrixReactionPreResponse, error) { +func (wa *WhatsAppClient) PreHandleMatrixReaction(_ context.Context, msg *bridgev2.MatrixReaction) (bridgev2.MatrixReactionPreResponse, error) { portalJID, err := waid.ParsePortalID(msg.Portal.ID) if err != nil { return bridgev2.MatrixReactionPreResponse{}, fmt.Errorf("failed to parse portal ID: %w", err) } else if portalJID == types.StatusBroadcastJID { return bridgev2.MatrixReactionPreResponse{}, ErrBroadcastReactionUnsupported } - sender := wa.GetLID() - if portalJID.Server == types.DefaultUserServer { - zerolog.Ctx(ctx).Warn().Stringer("portal_jid", portalJID).Msg("Matrix reaction received in phone number portal") - sender = wa.JID + sender := wa.JID + if portalJID.Server == types.HiddenUserServer || + msg.Portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup || + msg.Portal.Metadata.(*waid.PortalMetadata).AddressingMode == types.AddressingModeLID { + sender = wa.GetStore().GetLID() } return bridgev2.MatrixReactionPreResponse{ SenderID: waid.MakeUserID(sender), @@ -159,7 +153,7 @@ func (wa *WhatsAppClient) PreHandleMatrixReaction(ctx context.Context, msg *brid }, nil } -func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (result *database.Reaction, retErr error) { +func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (*database.Reaction, error) { messageID, err := waid.ParseMessageID(msg.TargetMessage.ID) if err != nil { return nil, fmt.Errorf("failed to parse target message ID: %w", err) @@ -176,10 +170,9 @@ func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev SenderTimestampMS: proto.Int64(msg.Event.Timestamp), }, } - defer wa.mcTrack(msg, time.Now(), &retErr) var req whatsmeow.SendRequestExtra 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 { return nil, fmt.Errorf("failed to encrypt reaction: %w", err) } @@ -198,7 +191,7 @@ func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev }, err } -func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) (retErr error) { +func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) error { messageID, err := waid.ParseMessageID(msg.TargetReaction.MessageID) if err != nil { return fmt.Errorf("failed to parse target message ID: %w", err) @@ -222,13 +215,12 @@ func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *b extra.ID = types.MessageID(msg.InputTransactionID) } - defer wa.mcTrack(msg, time.Now(), &retErr) resp, err := wa.Client.SendMessage(ctx, portalJID, reactionMsg, extra) zerolog.Ctx(ctx).Trace().Any("response", resp).Msg("WhatsApp reaction response") return err } -func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.MatrixEdit) (retErr error) { +func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.MatrixEdit) error { log := zerolog.Ctx(ctx) var editID types.MessageID @@ -252,8 +244,6 @@ func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.M if err != nil { return fmt.Errorf("failed to convert message: %w", err) } - defer wa.mcTrack(edit, time.Now(), &retErr) - convertedEdit := wa.Client.BuildEdit(messageID.Chat, messageID.ID, waMsg) if edit.OrigSender == nil { convertedEdit.EditedMessage.Message.ProtocolMessage.TimestampMS = proto.Int64(edit.Event.Timestamp) @@ -268,7 +258,7 @@ func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.M return err } -func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) (retErr error) { +func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) error { log := zerolog.Ctx(ctx) messageID, err := waid.ParseMessageID(msg.TargetMessage.ID) if err != nil { @@ -280,7 +270,6 @@ func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *br return fmt.Errorf("failed to parse portal ID: %w", err) } - defer wa.mcTrack(msg, time.Now(), &retErr) revokeMessage := wa.Client.BuildRevoke(messageID.Chat, messageID.Sender, messageID.ID) extra := whatsmeow.SendRequestExtra{} @@ -293,7 +282,7 @@ func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *br return err } -func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bridgev2.MatrixReadReceipt) (retErr error) { +func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bridgev2.MatrixReadReceipt) error { if !receipt.ReadUpTo.After(receipt.LastRead) { return nil } @@ -322,7 +311,7 @@ func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt * if err != nil { continue } - if wa.IsOwnJID(parsed.Sender) { + if parsed.Sender.User == wa.GetStore().GetLID().User || parsed.Sender.User == wa.JID.User { continue } var key types.JID @@ -332,7 +321,6 @@ func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt * } messagesToRead[key] = append(messagesToRead[key], parsed.ID) } - defer wa.mcTrack(receipt, time.Now(), &retErr) for messageSender, ids := range messagesToRead { err = wa.Client.MarkRead(ctx, ids, receipt.Receipt.Timestamp, portalJID, messageSender) if err != nil { @@ -342,7 +330,7 @@ func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt * return err } -func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.MatrixTyping) (retErr error) { +func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.MatrixTyping) error { portalJID, err := waid.ParsePortalID(msg.Portal.ID) if err != nil { return err @@ -363,7 +351,6 @@ func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2. return nil } - defer wa.mcTrack(msg, time.Now(), &retErr) if wa.Main.Config.SendPresenceOnTyping { err = wa.updatePresence(ctx, types.PresenceAvailable) if err != nil { @@ -375,7 +362,7 @@ func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2. var errUnsupportedDisappearingTimer = bridgev2.WrapErrorInStatus(errors.New("invalid value for disappearing timer")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true) -func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg *bridgev2.MatrixDisappearingTimer) (ok bool, retErr error) { +func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg *bridgev2.MatrixDisappearingTimer) (bool, error) { portalJID, err := waid.ParsePortalID(msg.Portal.ID) if err != nil { return false, err @@ -387,7 +374,6 @@ func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg return false, fmt.Errorf("%w (%s)", errUnsupportedDisappearingTimer, msg.Content.Timer.Duration) } - defer wa.mcTrack(msg, time.Now(), &retErr) settingTS := time.UnixMilli(msg.Event.Timestamp) err = wa.Client.SetDisappearingTimer(ctx, portalJID, msg.Content.Timer.Duration, settingTS) if err != nil { @@ -404,11 +390,7 @@ func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg return true, nil } -func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (result *bridgev2.MatrixMembershipResult, retErr error) { - if msg.Type.IsSelf && msg.OrigSender != nil { - return nil, nil - } - +func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (*bridgev2.MatrixMembershipResult, error) { portalJID, err := waid.ParsePortalID(msg.Portal.ID) if err != nil { return nil, err @@ -448,7 +430,6 @@ func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridg return nil, fmt.Errorf("cannot get target intent: unknown type: %T", target) } - defer wa.mcTrack(msg, time.Now(), &retErr) resp, err := wa.Client.UpdateGroupParticipants(ctx, portalJID, changes, action) if err != nil { return nil, err @@ -464,7 +445,7 @@ func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridg return &bridgev2.MatrixMembershipResult{RedirectTo: waid.MakeUserID(resp[0].JID)}, nil } -func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev2.MatrixRoomName) (ok bool, retErr error) { +func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev2.MatrixRoomName) (bool, error) { portalJID, err := waid.ParsePortalID(msg.Portal.ID) if err != nil { return false, err @@ -474,7 +455,6 @@ func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev return false, fmt.Errorf("cannot set room name for DM") } - defer wa.mcTrack(msg, time.Now(), &retErr) err = wa.Client.SetGroupName(ctx, portalJID, msg.Content.Name) if err != nil { return false, err @@ -486,7 +466,7 @@ func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev return true, nil } -func (wa *WhatsAppClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridgev2.MatrixRoomTopic) (ok bool, retErr error) { +func (wa *WhatsAppClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridgev2.MatrixRoomTopic) (bool, error) { portalJID, err := waid.ParsePortalID(msg.Portal.ID) if err != nil { return false, err @@ -496,7 +476,6 @@ func (wa *WhatsAppClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridge return false, fmt.Errorf("cannot set room topic for DM") } - defer wa.mcTrack(msg, time.Now(), &retErr) newID := wa.Client.GenerateMessageID() oldID := msg.Portal.Metadata.(*waid.PortalMetadata).TopicID err = wa.Client.SetGroupTopic(ctx, portalJID, oldID, newID, msg.Content.Topic) @@ -511,7 +490,7 @@ func (wa *WhatsAppClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridge return true, nil } -func (wa *WhatsAppClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridgev2.MatrixRoomAvatar) (ok bool, retErr error) { +func (wa *WhatsAppClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridgev2.MatrixRoomAvatar) (bool, error) { portalJID, err := waid.ParsePortalID(msg.Portal.ID) if err != nil { return false, err @@ -534,7 +513,6 @@ func (wa *WhatsAppClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridg } } - defer wa.mcTrack(msg, time.Now(), &retErr) avatarID, err := wa.Client.SetGroupPhoto(ctx, portalJID, data) if err != nil { return false, err @@ -602,7 +580,7 @@ func convertRoomAvatar(data []byte) ([]byte, error) { return buf.Bytes(), nil } -func (wa *WhatsAppClient) HandleMute(ctx context.Context, msg *bridgev2.MatrixMute) (retErr error) { +func (wa *WhatsAppClient) HandleMute(ctx context.Context, msg *bridgev2.MatrixMute) error { chatJID, err := waid.ParsePortalID(msg.Portal.ID) if err != nil { return err @@ -613,16 +591,14 @@ func (wa *WhatsAppClient) HandleMute(ctx context.Context, msg *bridgev2.MatrixMu if !muted || mutedUntil == event.MutedForever { muteTS = nil } - defer wa.mcTrack(msg, time.Now(), &retErr) return wa.Client.SendAppState(ctx, appstate.BuildMuteAbs(chatJID, muted, muteTS)) } -func (wa *WhatsAppClient) HandleRoomTag(ctx context.Context, msg *bridgev2.MatrixRoomTag) (retErr error) { +func (wa *WhatsAppClient) HandleRoomTag(ctx context.Context, msg *bridgev2.MatrixRoomTag) error { chatJID, err := waid.ParsePortalID(msg.Portal.ID) if err != nil { return err } - defer wa.mcTrack(msg, time.Now(), &retErr) _, isFavorite := msg.Content.Tags[event.RoomTagFavourite] return wa.Client.SendAppState(ctx, appstate.BuildPin(chatJID, isFavorite)) } @@ -638,7 +614,7 @@ func (wa *WhatsAppClient) getLastMessageInfo(ctx context.Context, chatJID types. lastTS = msgs[0].Timestamp parsed, _ := waid.ParseMessageID(msgs[0].ID) if parsed != nil { - fromMe := wa.IsOwnJID(parsed.Sender) + fromMe := parsed.Sender.ToNonAD() == wa.JID.ToNonAD() || parsed.Sender.ToNonAD() == wa.GetStore().GetLID().ToNonAD() var participant *string if chatJID.Server == types.GroupServer { participant = ptr.Ptr(parsed.Sender.String()) @@ -654,7 +630,7 @@ func (wa *WhatsAppClient) getLastMessageInfo(ctx context.Context, chatJID types. return lastTS, lastKey, nil } -func (wa *WhatsAppClient) HandleMarkedUnread(ctx context.Context, msg *bridgev2.MatrixMarkedUnread) (retErr error) { +func (wa *WhatsAppClient) HandleMarkedUnread(ctx context.Context, msg *bridgev2.MatrixMarkedUnread) error { chatJID, err := waid.ParsePortalID(msg.Portal.ID) if err != nil { return err @@ -663,11 +639,10 @@ func (wa *WhatsAppClient) HandleMarkedUnread(ctx context.Context, msg *bridgev2. if err != nil { return err } - defer wa.mcTrack(msg, time.Now(), &retErr) return wa.Client.SendAppState(ctx, appstate.BuildMarkChatAsRead(chatJID, msg.Content.Unread, lastTS, lastKey)) } -func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridgev2.MatrixDeleteChat) (retErr error) { +func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridgev2.MatrixDeleteChat) error { chatJID, err := waid.ParsePortalID(msg.Portal.ID) if err != nil { return err @@ -688,6 +663,5 @@ func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridg if err != nil { return err } - defer wa.mcTrack(msg, time.Now(), &retErr) return wa.Client.SendAppState(ctx, appstate.BuildDeleteChat(chatJID, lastTS, lastKey, true)) } diff --git a/pkg/connector/handlewhatsapp.go b/pkg/connector/handlewhatsapp.go index 509c589..8786685 100644 --- a/pkg/connector/handlewhatsapp.go +++ b/pkg/connector/handlewhatsapp.go @@ -28,6 +28,7 @@ import ( "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/appstate" "go.mau.fi/whatsmeow/proto/waE2E" + "go.mau.fi/whatsmeow/store" "go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types/events" "maunium.net/go/mautrix/bridgev2" @@ -76,7 +77,6 @@ func init() { func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { log := wa.UserLogin.Log ctx := log.WithContext(wa.Main.Bridge.BackgroundCtx) - wa.MC.OnWhatsAppEvent(rawEvt) success = true switch evt := rawEvt.(type) { @@ -111,9 +111,12 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { success = wa.handleWAPin(evt) case *events.HistorySync: - wa.UserLogin.Log.Warn().Msg("Unexpected history sync event received") + if wa.Main.Bridge.Config.Backfill.Enabled { + wa.historySyncs <- evt.Data + } case *events.MediaRetry: - success = wa.handleWAMediaRetry(ctx, evt) + wa.phoneSeen(evt.Timestamp) + success = wa.UserLogin.QueueRemoteEvent(&WAMediaRetry{MediaRetry: evt, wa: wa}).Success case *events.GroupInfo: success = wa.handleWAGroupInfoChange(ctx, evt) @@ -125,20 +128,6 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { success = wa.handleWANewsletterLeave(evt) case *events.Picture: success = wa.handleWAPictureUpdate(ctx, evt) - case *events.NotifyAccountReachoutTimelock: - wa.UserLogin.TrackAnalytics("WhatsApp Account Reachout Timelock", map[string]any{ - "enforcement_type": evt.EnforcementType, - "is_active": evt.IsActive, - "time_enforcement_ends": evt.TimeEnforcementEnds.Time, - }) - wa.UserLogin.Metadata.(*waid.UserLoginMetadata).ReachoutTimelockUntil = evt.TimeEnforcementEnds.Time - if wa.UserLogin.BridgeState.GetPrevUnsent().StateEvent == status.StateConnected { - wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected}) - } - err := wa.UserLogin.Save(ctx) - if err != nil { - log.Err(err).Msg("Failed to save user login metadata after reachout timelock update") - } case *events.AppStateSyncComplete: wa.handleWAAppStateSyncComplete(ctx, evt) @@ -157,7 +146,7 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { if err != nil { log.Err(err).Msg("Failed to update push name in store") } - _, _, err = wa.GetStore().Contacts.PutPushName(ctx, wa.GetLID().ToNonAD(), evt.Action.GetName()) + _, _, err = wa.GetStore().Contacts.PutPushName(ctx, wa.GetStore().GetLID().ToNonAD(), evt.Action.GetName()) if err != nil { log.Err(err).Msg("Failed to update push name in store") } @@ -181,6 +170,7 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { }() go wa.syncRemoteProfile(ctx, nil) } + wa.MC.OnConnect(store.GetWAVersion()[2], wa.Device.Platform) case *events.OfflineSyncPreview: log.Info(). Int("message_count", evt.Messages). @@ -258,40 +248,115 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { return } -func (wa *WhatsAppClient) ensureAltJIDs(ctx context.Context, info *types.MessageSource, checkPhones bool) bool { - var err error - if info.Sender.Server == types.DefaultUserServer && info.SenderAlt.IsEmpty() { - info.SenderAlt, err = wa.GetStore().LIDs.GetLIDForPN(ctx, info.Sender) +func (wa *WhatsAppClient) rerouteWAMessage(ctx context.Context, evtType string, info *types.MessageSource, msgID any) { + if (info.Chat.Server == types.HiddenUserServer || info.Chat.Server == types.BroadcastServer) && + info.Sender.Server == types.HiddenUserServer && info.SenderAlt.IsEmpty() { + info.SenderAlt, _ = wa.GetStore().LIDs.GetPNForLID(ctx, info.Sender) + } + if info.Chat.Server == types.HiddenUserServer && info.IsFromMe && info.RecipientAlt.IsEmpty() { + info.RecipientAlt, _ = wa.GetStore().LIDs.GetPNForLID(ctx, info.Chat) + } + if info.Chat.Server == types.HiddenUserServer && info.Sender.ToNonAD() == info.Chat && info.SenderAlt.Server == types.DefaultUserServer { + wa.UserLogin.Log.Debug(). + Stringer("lid", info.Sender). + Stringer("pn", info.SenderAlt). + Any("message_id", msgID). + Str("evt_type", evtType). + Msg("Forced LID DM sender to phone number in incoming message") + info.Sender, info.SenderAlt = info.SenderAlt, info.Sender + info.Chat = info.Sender.ToNonAD() + } else if info.Chat.Server == types.HiddenUserServer && info.IsFromMe && info.RecipientAlt.Server == types.DefaultUserServer { + wa.UserLogin.Log.Debug(). + Stringer("lid", info.Chat). + Stringer("pn", info.RecipientAlt). + Any("message_id", msgID). + Str("evt_type", evtType). + Msg("Forced LID DM sender to phone number in own message sent from another device") + info.Chat = info.RecipientAlt.ToNonAD() + if info.Sender.Server == types.HiddenUserServer { + info.Sender, info.SenderAlt = info.SenderAlt, info.Sender + if info.Sender.IsEmpty() { + info.Sender = wa.GetStore().GetJID() + info.Sender.Device = info.SenderAlt.Device + } + } + } else if info.Chat.Server == types.BroadcastServer && info.Sender.Server == types.HiddenUserServer && info.SenderAlt.Server == types.DefaultUserServer { + wa.UserLogin.Log.Debug(). + Stringer("lid", info.Sender). + Stringer("pn", info.SenderAlt). + Stringer("chat", info.Chat). + Any("message_id", msgID). + Str("evt_type", evtType). + Msg("Forced LID broadcast list sender to phone number in incoming message") + info.Sender, info.SenderAlt = info.SenderAlt, info.Sender + } else if info.Sender.Server == types.BotServer && info.Chat.Server == types.HiddenUserServer { + chatPN, err := wa.GetStore().LIDs.GetPNForLID(ctx, info.Chat) if err != nil { - zerolog.Ctx(ctx).Err(err).Stringer("sender", info.Sender).Msg("Failed to get LID for sender") - return false + wa.UserLogin.Log.Err(err). + Any("message_id", msgID). + Stringer("lid", info.Chat). + Str("evt_type", evtType). + Msg("Failed to get phone number of DM for incoming bot message") + } else if !chatPN.IsEmpty() { + wa.UserLogin.Log.Debug(). + Stringer("lid", info.Chat). + Stringer("pn", chatPN). + Any("message_id", msgID). + Str("evt_type", evtType). + Msg("Forced LID chat to phone number in bot message") + info.Chat = chatPN } } - if info.Chat.Server == types.DefaultUserServer && info.IsFromMe && info.RecipientAlt.IsEmpty() { - info.RecipientAlt, err = wa.GetStore().LIDs.GetLIDForPN(ctx, info.Chat) - if err != nil { - zerolog.Ctx(ctx).Err(err).Stringer("chat", info.Chat).Msg("Failed to get LID for chat") - return false - } - } - if checkPhones { - return wa.checkAllPhonesInMessage(ctx, info) - } - return true } func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Message) (success bool) { success = true + wa.rerouteWAMessage(ctx, "message", &evt.Info.MessageSource, evt.Info.ID) + wa.UserLogin.Log.Trace(). + Any("info", evt.Info). + Any("payload", evt.Message). + Msg("Received WhatsApp message") if evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast { return } - if evt.Info.Chat.Server == types.NewsletterServer && wa.disableNewsletter { + if evt.Info.IsFromMe && + evt.Message.GetProtocolMessage().GetHistorySyncNotification() != nil && + wa.Main.Bridge.Config.Backfill.Enabled && + wa.Client.ManualHistorySyncDownload { + wa.saveWAHistorySyncNotification(ctx, evt.Message.ProtocolMessage.HistorySyncNotification) + } + + messageAssoc := evt.Message.GetMessageContextInfo().GetMessageAssociation() + if assocType := messageAssoc.GetAssociationType(); assocType == waE2E.MessageAssociation_HD_IMAGE_DUAL_UPLOAD || assocType == waE2E.MessageAssociation_HD_VIDEO_DUAL_UPLOAD { + parentKey := messageAssoc.GetParentMessageKey() + associatedMessage := evt.Message.GetAssociatedChildMessage().GetMessage() + wa.UserLogin.Log.Debug(). + Str("message_id", evt.Info.ID). + Str("parent_id", parentKey.GetID()). + Stringer("assoc_type", assocType). + Msg("Received HD replacement message, converting to edit") + + protocolMsg := &waE2E.ProtocolMessage{ + Type: waE2E.ProtocolMessage_MESSAGE_EDIT.Enum(), + Key: parentKey, + EditedMessage: associatedMessage, + } + evt.Message = &waE2E.Message{ + ProtocolMessage: protocolMsg, + } + } else if assocType == waE2E.MessageAssociation_MOTION_PHOTO { + //evt.Message = evt.Message.GetAssociatedChildMessage().GetMessage() + wa.UserLogin.Log.Debug(). + Str("message_id", evt.Info.ID). + Str("parent_id", messageAssoc.GetParentMessageKey().GetID()). + Msg("Ignoring motion photo update") return } - if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, true) { - return false - } + parsedMessageType := getMessageType(evt.Message) + if parsedMessageType == "ignore" || strings.HasPrefix(parsedMessageType, "unknown_protocol_") { + return + } if encReact := evt.Message.GetEncReactionMessage(); encReact != nil { decrypted, err := wa.Client.DecryptReaction(ctx, evt) if err != nil { @@ -313,65 +378,13 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa if encMessage := evt.Message.GetSecretEncryptedMessage(); encMessage != nil { decrypted, err := wa.Client.DecryptSecretEncryptedMessage(ctx, evt) if err != nil { - wa.UserLogin.Log.Err(err). - Str("message_id", evt.Info.ID). - Stringer("evt_sender", evt.Info.Sender). - Any("target_message_key", encMessage.TargetMessageKey). - Msg("Failed to decrypt secret-encrypted message") + wa.UserLogin.Log.Err(err).Str("message_id", evt.Info.ID).Msg("Failed to decrypt message") return } evt.RawMessage = decrypted evt.UnwrapRaw() parsedMessageType = getMessageType(evt.Message) } - wa.UserLogin.Log.Trace(). - Any("info", evt.Info). - Any("payload", evt.Message). - Msg("Received WhatsApp message") - if evt.Info.IsFromMe && - evt.Message.GetProtocolMessage().GetHistorySyncNotification() != nil && - wa.Main.Bridge.Config.Backfill.Enabled { - wa.saveWAHistorySyncNotification(ctx, evt.Message.ProtocolMessage.HistorySyncNotification) - } - if parsedMessageType == "ignore" || strings.HasPrefix(parsedMessageType, "unknown_protocol_") { - return - } - - dontRenderEdited := false - messageAssoc := evt.Message.GetMessageContextInfo().GetMessageAssociation() - if assocType := messageAssoc.GetAssociationType(); assocType == waE2E.MessageAssociation_HD_IMAGE_DUAL_UPLOAD || assocType == waE2E.MessageAssociation_HD_VIDEO_DUAL_UPLOAD { - parentKey := messageAssoc.GetParentMessageKey() - protocolMsg := evt.Message.GetProtocolMessage() - if protocolMsg.GetType() != waE2E.ProtocolMessage_MESSAGE_EDIT || protocolMsg.GetKey() == nil { - protocolMsg = &waE2E.ProtocolMessage{ - Type: waE2E.ProtocolMessage_MESSAGE_EDIT.Enum(), - Key: parentKey, - EditedMessage: evt.Message.GetAssociatedChildMessage().GetMessage(), - } - dontRenderEdited = true - } else if child := protocolMsg.GetEditedMessage().GetAssociatedChildMessage().GetMessage(); child != nil { - protocolMsg.EditedMessage = child - protocolMsg.Key = parentKey - } - wa.UserLogin.Log.Debug(). - Str("message_id", evt.Info.ID). - Str("parent_id", parentKey.GetID()). - Stringer("assoc_type", assocType). - Msg("Received HD replacement message, converting to edit") - - evt.Message = &waE2E.Message{ - ProtocolMessage: protocolMsg, - } - parsedMessageType = getMessageType(evt.Message) - } else if assocType == waE2E.MessageAssociation_MOTION_PHOTO { - //evt.Message = evt.Message.GetAssociatedChildMessage().GetMessage() - wa.UserLogin.Log.Debug(). - Str("message_id", evt.Info.ID). - Str("parent_id", messageAssoc.GetParentMessageKey().GetID()). - Msg("Ignoring motion photo update") - return - } - res := wa.UserLogin.QueueRemoteEvent(&WAMessageEvent{ MessageInfoWrapper: &MessageInfoWrapper{ Info: evt.Info, @@ -381,75 +394,39 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa MsgEvent: evt, parsedMessageType: parsedMessageType, - dontRenderEdited: dontRenderEdited, }) return res.Success } func (wa *WhatsAppClient) handleWAUndecryptableMessage(ctx context.Context, evt *events.UndecryptableMessage) bool { - if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, true) { - return false - } + wa.rerouteWAMessage(ctx, "undecryptable message", &evt.Info.MessageSource, evt.Info.ID) wa.UserLogin.Log.Debug(). Any("info", evt.Info). Bool("unavailable", evt.IsUnavailable). Str("decrypt_fail", string(evt.DecryptFailMode)). Msg("Received undecryptable WhatsApp message") wa.trackUndecryptable(evt) - if evt.DecryptFailMode == events.DecryptFailHide { - return true - } if evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast { return true } + hidden := evt.DecryptFailMode == events.DecryptFailHide res := wa.UserLogin.QueueRemoteEvent(&WAUndecryptableMessage{ MessageInfoWrapper: &MessageInfoWrapper{ Info: evt.Info, wa: wa, }, - Type: evt.UnavailableType, - }) - 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, + Type: evt.UnavailableType, + Hidden: hidden, }) return res.Success } func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Receipt) (success bool) { + origChat := evt.Chat + wa.rerouteWAMessage(ctx, "receipt", &evt.MessageSource, evt.MessageIDs) if evt.IsFromMe && evt.Sender.Device == 0 { wa.phoneSeen(evt.Timestamp) } - if !wa.ensureAltJIDs(ctx, &evt.MessageSource, true) { - return false - } var evtType bridgev2.RemoteEventType switch evt.Type { case types.ReceiptTypeRead, types.ReceiptTypeReadSelf: @@ -461,34 +438,28 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei default: return true } - targets := make([]networkid.MessageID, 0, len(evt.MessageIDs)) - messageSender := wa.GetLID() + targets := make([]networkid.MessageID, len(evt.MessageIDs)) + messageSender := wa.JID if !evt.MessageSender.IsEmpty() { messageSender = evt.MessageSender - } - var chatAlt types.JID - if evt.Chat.Server == types.DefaultUserServer { - chatLID, _ := wa.GetStore().LIDs.GetLIDForPN(ctx, evt.Chat) - if !chatLID.IsEmpty() { - chatAlt = evt.Chat - evt.Chat = chatLID + // Second part of rerouting receipts in LID chats + if messageSender == origChat && evt.Chat != origChat { + messageSender = evt.Chat + } + } else if evt.Chat.Server == types.GroupServer && evt.Sender.Server == types.HiddenUserServer { + lid := wa.GetStore().GetLID() + if !lid.IsEmpty() { + messageSender = lid } } - for _, id := range evt.MessageIDs { - targets = append(targets, waid.MakeMessageID(evt.Chat, messageSender, id)) - if !chatAlt.IsEmpty() { - targets = append(targets, waid.MakeMessageID(chatAlt, messageSender, id)) - } - } - senderLID := evt.Sender - if senderLID.Server == types.DefaultUserServer && !evt.SenderAlt.IsEmpty() { - senderLID = evt.SenderAlt + for i, id := range evt.MessageIDs { + targets[i] = waid.MakeMessageID(evt.Chat, messageSender, id) } res := wa.UserLogin.QueueRemoteEvent(&simplevent.Receipt{ EventMeta: simplevent.EventMeta{ Type: evtType, PortalKey: wa.makeWAPortalKey(evt.Chat), - Sender: wa.makeEventSender(ctx, senderLID), + Sender: wa.makeEventSender(ctx, evt.Sender), Timestamp: evt.Timestamp, }, Targets: targets, @@ -497,11 +468,11 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei } func (wa *WhatsAppClient) handleWAChatPresence(ctx context.Context, evt *events.ChatPresence) { - if evt.Chat.Server == types.DefaultUserServer && evt.Sender.ToNonAD() == evt.Chat { + if evt.Chat.Server == types.HiddenUserServer && evt.Sender.ToNonAD() == evt.Chat { if evt.SenderAlt.IsEmpty() { - evt.SenderAlt, _ = wa.GetStore().LIDs.GetLIDForPN(ctx, evt.Sender) + evt.SenderAlt, _ = wa.GetStore().LIDs.GetPNForLID(ctx, evt.Sender) } - if evt.SenderAlt.Server == types.HiddenUserServer { + if evt.SenderAlt.Server == types.DefaultUserServer { evt.Sender, evt.SenderAlt = evt.SenderAlt, evt.Sender evt.Chat = evt.Sender.ToNonAD() } @@ -535,10 +506,9 @@ func (wa *WhatsAppClient) handleWALogout(reason events.ConnectFailureReason, onC } else if reason == events.ConnectFailureMainDeviceGone { errorCode = WAMainDeviceGone } - wa.Disconnect() + wa.Client.Disconnect() wa.Client = nil wa.JID = types.EmptyJID - wa.LID = types.EmptyJID wa.UserLogin.Metadata.(*waid.UserLoginMetadata).WADeviceID = 0 wa.UserLogin.BridgeState.Send(status.BridgeState{ StateEvent: status.StateBadCredentials, @@ -552,15 +522,12 @@ func (wa *WhatsAppClient) handleWACallStart(ctx context.Context, group, sender, if !wa.Main.Config.CallStartNotices || time.Since(ts) > callEventMaxAge { return true } - if sender.Server == types.DefaultUserServer && senderAlt.IsEmpty() { - senderAlt, _ = wa.GetStore().LIDs.GetLIDForPN(ctx, sender) - } - if sender.Server == types.DefaultUserServer && senderAlt.Server == types.HiddenUserServer { + if sender.Server == types.HiddenUserServer && senderAlt.Server == types.DefaultUserServer { wa.UserLogin.Log.Debug(). - Stringer("lid", senderAlt). - Stringer("pn", sender). + Stringer("lid", sender). + Stringer("pn", senderAlt). Str("call_id", id). - Msg("Forced phone number caller to LID in incoming call") + Msg("Forced LID caller to phone number in incoming call") sender, senderAlt = senderAlt, sender } chat := group @@ -607,12 +574,6 @@ func (wa *WhatsAppClient) handleWAIdentityChange(ctx context.Context, evt *event if !wa.Main.Config.IdentityChangeNotices { return } - if evt.JID.Server == types.DefaultUserServer { - lid, _ := wa.GetStore().LIDs.GetLIDForPN(ctx, evt.JID) - if !lid.IsEmpty() { - evt.JID = lid - } - } wa.UserLogin.QueueRemoteEvent(&simplevent.Message[*events.IdentityChange]{ EventMeta: simplevent.EventMeta{ Type: bridgev2.RemoteEventMessage, @@ -663,14 +624,13 @@ func (wa *WhatsAppClient) handleWADeleteChat(ctx context.Context, evt *events.De func (wa *WhatsAppClient) handleWADeleteForMe(ctx context.Context, evt *events.DeleteForMe) bool { chatJID := wa.maybeConvertJIDToLID(ctx, evt.ChatJID) - senderJID := wa.maybeConvertJIDToLID(ctx, evt.SenderJID) return wa.UserLogin.QueueRemoteEvent(&simplevent.MessageRemove{ EventMeta: simplevent.EventMeta{ Type: bridgev2.RemoteEventMessageRemove, PortalKey: wa.makeWAPortalKey(chatJID), Timestamp: evt.Timestamp, }, - TargetMessage: waid.MakeMessageID(chatJID, senderJID, evt.MessageID), + TargetMessage: waid.MakeMessageID(chatJID, evt.SenderJID, evt.MessageID), OnlyForMe: true, }).Success } @@ -681,7 +641,7 @@ func (wa *WhatsAppClient) handleWAMarkChatAsRead(ctx context.Context, evt *event EventMeta: simplevent.EventMeta{ Type: bridgev2.RemoteEventReadReceipt, PortalKey: wa.makeWAPortalKey(chatJID), - Sender: wa.makeEventSender(ctx, wa.GetLID()), + Sender: wa.makeEventSender(ctx, wa.JID), Timestamp: evt.Timestamp, }, ReadUpTo: evt.Timestamp, @@ -770,6 +730,9 @@ func (wa *WhatsAppClient) handleWAGroupInfoChange(ctx context.Context, evt *even } func (wa *WhatsAppClient) handleWAJoinedGroup(ctx context.Context, evt *events.JoinedGroup) bool { + if wa.createDedup.Pop(evt.CreateKey) { + return true + } return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{ EventMeta: simplevent.EventMeta{ Type: bridgev2.RemoteEventChatResync, @@ -782,9 +745,6 @@ func (wa *WhatsAppClient) handleWAJoinedGroup(ctx context.Context, evt *events.J } func (wa *WhatsAppClient) handleWANewsletterJoin(ctx context.Context, evt *events.NewsletterJoin) bool { - if wa.disableNewsletter { - return true - } return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{ EventMeta: simplevent.EventMeta{ Type: bridgev2.RemoteEventChatResync, @@ -883,12 +843,7 @@ func (wa *WhatsAppClient) handleWAAppStateSyncComplete(ctx context.Context, evt } else { log.Info(). Time("recovery_ts", ts). - Bool("recovery_evt", evt.Recovery). Msg("Unmarked app state recovery as attempted after successful full sync") - wa.UserLogin.TrackAnalytics("WhatsApp Appstate Recovery Success", map[string]any{ - "patch_name": evt.Name, - "from_recovery": evt.Recovery, - }) } } else if ts, exists = wa.appStateFullSyncAttempted[evt.Name]; exists { delete(wa.appStateFullSyncAttempted, evt.Name) @@ -945,9 +900,6 @@ func (wa *WhatsAppClient) handleWAAppStateSyncError(ctx context.Context, evt *ev if err != nil { log.Err(err).Msg("Failed to save login metadata after marking app state recovery as attempted") } - wa.UserLogin.TrackAnalytics("WhatsApp Appstate Recovery Request", map[string]any{ - "patch_name": evt.Name, - }) go func() { resp, err := wa.Client.SendPeerMessage(ctx, whatsmeow.BuildAppStateRecoveryRequest(evt.Name)) if err != nil { diff --git a/pkg/connector/id.go b/pkg/connector/id.go index c0aa06b..c07e431 100644 --- a/pkg/connector/id.go +++ b/pkg/connector/id.go @@ -49,7 +49,7 @@ func (wa *WhatsAppClient) makeEventSender(ctx context.Context, id types.JID) bri } } return bridgev2.EventSender{ - IsFromMe: wa.IsOwnJID(id), + IsFromMe: id.User == wa.GetStore().GetJID().User || id.User == wa.GetStore().GetLID().User, Sender: waid.MakeUserID(id), SenderLogin: waid.MakeUserLoginID(senderLoginJID), } @@ -60,25 +60,24 @@ func (wa *WhatsAppClient) messageIDToKey(id *waid.ParsedMessageID) *waCommon.Mes RemoteJID: ptr.Ptr(id.Chat.String()), ID: ptr.Ptr(id.ID), } - if wa.IsOwnJID(id.Sender) { + if id.Sender.User == wa.GetStore().GetJID().User || id.Sender.User == wa.GetStore().GetLID().User { key.FromMe = ptr.Ptr(true) } - if id.Chat.Server != types.MessengerServer && id.Chat.Server != types.DefaultUserServer && - id.Chat.Server != types.HiddenUserServer && id.Chat.Server != types.BotServer { + if id.Chat.Server != types.MessengerServer && id.Chat.Server != types.DefaultUserServer && id.Chat.Server != types.HiddenUserServer && id.Chat.Server != types.BotServer { key.Participant = ptr.Ptr(id.Sender.String()) } return key } -func (wa *WhatsAppClient) maybeConvertJIDToLID(ctx context.Context, jid types.JID) types.JID { - if jid.Server == types.DefaultUserServer { - if lidForPN, err := wa.GetStore().LIDs.GetLIDForPN(ctx, jid); err != nil { +func (wa *WhatsAppClient) maybeConvertJIDToLID(ctx context.Context, chatJID types.JID) types.JID { + if chatJID.Server == types.HiddenUserServer { + if pn, err := wa.GetStore().LIDs.GetPNForLID(ctx, chatJID); err != nil { wa.UserLogin.Log.Err(err). - Stringer("pn", jid). - Msg("Failed to get LID for phone number chat") - } else if !lidForPN.IsEmpty() { - return lidForPN + Stringer("lid", chatJID). + Msg("Failed to get phone number for LID chat") + } else if !pn.IsEmpty() { + return pn.ToNonAD() } } - return jid + return chatJID } diff --git a/pkg/connector/lidmigrate.go b/pkg/connector/lidmigrate.go deleted file mode 100644 index cfab099..0000000 --- a/pkg/connector/lidmigrate.go +++ /dev/null @@ -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 . - -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 -} diff --git a/pkg/connector/login.go b/pkg/connector/login.go index a62b294..88e7b75 100644 --- a/pkg/connector/login.go +++ b/pkg/connector/login.go @@ -2,7 +2,6 @@ package connector import ( "context" - "encoding/json" "errors" "fmt" "net/http" @@ -13,7 +12,6 @@ import ( "go.mau.fi/util/exsync" "go.mau.fi/util/jsontime" "go.mau.fi/whatsmeow" - "go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types/events" waLog "go.mau.fi/whatsmeow/util/log" "maunium.net/go/mautrix/bridgev2" @@ -27,7 +25,6 @@ const ( LoginStepIDQR = "fi.mau.whatsapp.login.qr" LoginStepIDPhoneNumber = "fi.mau.whatsapp.login.phone" LoginStepIDCode = "fi.mau.whatsapp.login.code" - LoginStepIDPasskey = "fi.mau.whatsapp.login.passkey" LoginStepIDComplete = "fi.mau.whatsapp.login.complete" LoginFlowIDQR = "qr" @@ -79,8 +76,8 @@ var ( } ErrRateLimitedByWhatsApp = bridgev2.RespError{ ErrCode: "FI.MAU.WHATSAPP.RATE_LIMITED", - Err: "Rate limited by WhatsApp. Try again later.", - StatusCode: http.StatusBadRequest, + Err: "Rate limited by WhatsApp", + StatusCode: http.StatusTooManyRequests, } ) @@ -94,11 +91,9 @@ func (wa *WhatsAppConnector) CreateLogin(_ context.Context, user *bridgev2.User, Bool("phone_code", flowID == LoginFlowIDPhone). Logger(), - WaitForQRs: exsync.NewEvent(), - LoginComplete: exsync.NewEvent(), - PasskeyRequest: exsync.NewEvent(), - PasskeyConfirmation: exsync.NewEvent(), - Received515: exsync.NewEvent(), + WaitForQRs: exsync.NewEvent(), + LoginComplete: exsync.NewEvent(), + Received515: exsync.NewEvent(), }, nil } @@ -119,11 +114,6 @@ type WALogin struct { Received515 *exsync.Event PrevQRIndex atomic.Int32 - PasskeyRequest *exsync.Event - PasskeyRequestData *events.PairPasskeyRequest - PasskeyConfirmation *exsync.Event - PasskeyConfirmationData *events.PairPasskeyConfirmation - Closed atomic.Bool EventHandlerID uint32 } @@ -132,11 +122,9 @@ var ( _ bridgev2.LoginProcessDisplayAndWait = (*WALogin)(nil) _ bridgev2.LoginProcessUserInput = (*WALogin)(nil) _ bridgev2.LoginProcessWithOverride = (*WALogin)(nil) - _ bridgev2.LoginProcessWebAuthn = (*WALogin)(nil) ) -const LoginConnectWait = 30 * time.Second -const LoginPairPhoneWait = 30 * time.Second +const LoginConnectWait = 15 * time.Second func (wl *WALogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) { wl.Main.firstClientConnectOnce.Do(wl.Main.onFirstClientConnect) @@ -194,21 +182,19 @@ func (wl *WALogin) StartWithOverride(ctx context.Context, old *bridgev2.UserLogi } func (wl *WALogin) SubmitUserInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) { + ctx, cancel := context.WithTimeout(ctx, LoginConnectWait) + defer cancel() err := wl.Client.Connect() if err != nil { wl.Log.Err(err).Msg("Failed to connect to WhatsApp for phone code login") return nil, err } - connectCtx, cancelConnect := context.WithTimeout(ctx, LoginConnectWait) - err = wl.WaitForQRs.Wait(connectCtx) - cancelConnect() + err = wl.WaitForQRs.Wait(ctx) if err != nil { wl.Log.Warn().Err(err).Msg("Timed out waiting for connection") return nil, fmt.Errorf("failed to wait for connection: %w", err) } - pairCtx, cancelPair := context.WithTimeout(ctx, LoginPairPhoneWait) - defer cancelPair() - pairingCode, err := wl.Client.PairPhone(pairCtx, input["phone_number"], true, whatsmeow.PairClientChrome, "Chrome (Linux)") + pairingCode, err := wl.Client.PairPhone(ctx, input["phone_number"], true, whatsmeow.PairClientChrome, "Chrome (Linux)") if err != nil { wl.Log.Err(err).Msg("Failed to request phone code login") if errors.Is(err, whatsmeow.ErrPhoneNumberTooShort) { @@ -276,17 +262,6 @@ func (wl *WALogin) handleEvent(rawEvt any) { case *events.ClientOutdated: wl.Log.Error().Msg("Got client outdated error") wl.LoginError = ErrLoginClientOutdated - case *events.PairPasskeyRequest: - wl.PasskeyRequestData = evt - wl.PasskeyRequest.Set() - return - case *events.PairPasskeyConfirmation: - wl.PasskeyConfirmationData = evt - wl.PasskeyConfirmation.Set() - return - case *events.PairPasskeyError: - wl.Log.Err(evt.Error).Msg("Got passkey error") - wl.LoginError = evt.Error case *events.PairSuccess: wl.Log.Info().Any("event_data", evt).Msg("Got pair successful event") wl.LoginSuccess = evt @@ -314,14 +289,10 @@ func (wl *WALogin) handleEvent(rawEvt any) { func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) { if wl.PhoneCode { - select { - case <-ctx.Done(): + err := wl.LoginComplete.Wait(ctx) + if err != nil { wl.Cancel() - return nil, ctx.Err() - case <-wl.PasskeyRequest.GetChan(): - return wl.makePasskeyStep() - case <-wl.LoginComplete.GetChan(): - // continue + return nil, err } } else { prevIndex := int(wl.PrevQRIndex.Load()) @@ -348,16 +319,9 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) { case <-ctx.Done(): wl.Cancel() return nil, ctx.Err() - case <-wl.PasskeyRequest.GetChan(): - return wl.makePasskeyStep() case <-wl.LoginComplete.GetChan(): - // continue } } - return wl.onLoginComplete(ctx) -} - -func (wl *WALogin) onLoginComplete(ctx context.Context) (*bridgev2.LoginStep, error) { if wl.LoginError != nil { wl.Log.Debug().Err(wl.LoginError).Msg("Login completed with error") wl.Cancel() @@ -408,66 +372,6 @@ func (wl *WALogin) onLoginComplete(ctx context.Context) (*bridgev2.LoginStep, er }, nil } -func (wl *WALogin) makePasskeyStep() (*bridgev2.LoginStep, error) { - pubkeyData, err := json.Marshal(wl.PasskeyRequestData.PublicKey) - if err != nil { - wl.Log.Err(err).Msg("Failed to marshal public key data") - return nil, err - } - return &bridgev2.LoginStep{ - Type: bridgev2.LoginStepTypeWebAuthn, - StepID: LoginStepIDPasskey, - WebAuthnParams: &bridgev2.LoginWebAuthnParams{ - URL: "https://web.whatsapp.com", - PublicKey: pubkeyData, - }, - }, nil -} - -func (wl *WALogin) SubmitWebAuthnResponse(ctx context.Context, rawResp json.RawMessage) (*bridgev2.LoginStep, error) { - var resp types.WebAuthnResponse - err := json.Unmarshal(rawResp, &resp) - if err != nil { - wl.Log.Err(err).Msg("Failed to unmarshal WebAuthn response") - wl.Cancel() - return nil, err - } - err = wl.Client.SendPasskeyResponse(ctx, &resp) - if err != nil { - wl.Log.Err(err).Msg("Failed to send WebAuthn response") - wl.Cancel() - return nil, err - } - select { - case <-ctx.Done(): - wl.Cancel() - return nil, ctx.Err() - case <-wl.PasskeyConfirmation.GetChan(): - if !wl.PasskeyConfirmationData.SkipHandoffUX { - wl.Cancel() - return nil, bridgev2.RespError{ - ErrCode: "FI.MAU.WHATSAPP.NOT_IMPLEMENTED", - Err: "Displaying WebAuthn pairing confirmation codes is not yet implemented", - StatusCode: http.StatusBadRequest, - } - } - err = wl.Client.SendPasskeyConfirmation(ctx) - if err != nil { - wl.Log.Err(err).Msg("Failed to send WebAuthn confirmation") - wl.Cancel() - return nil, err - } - case <-wl.LoginComplete.GetChan(): - } - select { - case <-ctx.Done(): - wl.Cancel() - return nil, ctx.Err() - case <-wl.LoginComplete.GetChan(): - } - return wl.onLoginComplete(ctx) -} - func (wl *WALogin) Cancel() { wl.Closed.Store(true) wl.Client.RemoveEventHandler(wl.EventHandlerID) diff --git a/pkg/connector/mclient.go b/pkg/connector/mclient.go index f85545a..0930617 100644 --- a/pkg/connector/mclient.go +++ b/pkg/connector/mclient.go @@ -21,7 +21,6 @@ import ( "encoding/json" "time" - "go.mau.fi/util/ptr" "go.mau.fi/whatsmeow" waBinary "go.mau.fi/whatsmeow/binary" "go.mau.fi/whatsmeow/types" @@ -40,16 +39,14 @@ func (wa *WhatsAppClient) initMC() { } type mClient = interface { - OnMatrixEvent(any, time.Duration, error) - OnWhatsAppEvent(any) + OnConnect(version uint32, platform string) } type noopMC struct{} var noopMCInstance mClient = &noopMC{} -func (n *noopMC) OnMatrixEvent(any, time.Duration, error) {} -func (n *noopMC) OnWhatsAppEvent(any) {} +func (n *noopMC) OnConnect(version uint32, platform string) {} type mWAClient = interface { MSend(data []byte) @@ -82,7 +79,3 @@ func (wa *WhatsAppClient) MSave(s json.RawMessage) { wa.UserLogin.Log.Err(err).Msg("Failed to save MC data") } } - -func (wa *WhatsAppClient) mcTrack(evt any, start time.Time, err *error) { - wa.MC.OnMatrixEvent(evt, time.Since(start), ptr.Val(err)) -} diff --git a/pkg/connector/mediarequest.go b/pkg/connector/mediarequest.go index 3806196..2c382d1 100644 --- a/pkg/connector/mediarequest.go +++ b/pkg/connector/mediarequest.go @@ -70,9 +70,10 @@ func (wa *WhatsAppClient) processFailedMedia(ctx context.Context, portalKey netw func (wa *WhatsAppClient) mediaRequestLoop(ctx context.Context) { log := wa.UserLogin.Log.With().Str("loop", "media requests").Logger() ctx = log.WithContext(ctx) - userTz, err := wa.UserLogin.Metadata.(*waid.UserLoginMetadata).LoadTimezone() + tzName := wa.UserLogin.Metadata.(*waid.UserLoginMetadata).Timezone + userTz, err := time.LoadLocation(tzName) var startIn time.Duration - if err == nil && userTz != nil { + if tzName != "" && err == nil && userTz != nil { now := time.Now() startAt := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, userTz) startAt = startAt.Add(time.Duration(wa.Main.Config.HistorySync.MediaRequests.RequestLocalTime) * time.Minute) @@ -155,8 +156,8 @@ func (wa *WhatsAppClient) sendMediaRequestDirect(ctx context.Context, rawMsgID n return wa.Client.SendMediaRetryReceipt(ctx, &types.MessageInfo{ ID: msgID.ID, MessageSource: types.MessageSource{ - IsFromMe: wa.IsOwnJID(msgID.Sender), - IsGroup: msgID.Chat.Server != types.DefaultUserServer && msgID.Chat.Server != types.HiddenUserServer && msgID.Chat.Server != types.BotServer, + IsFromMe: msgID.Sender.User == wa.JID.User, + IsGroup: msgID.Chat.Server != types.DefaultUserServer && msgID.Chat.Server != types.BotServer, Sender: msgID.Sender, Chat: msgID.Chat, }, diff --git a/pkg/connector/phoneping.go b/pkg/connector/phoneping.go index 71094c9..ce38079 100644 --- a/pkg/connector/phoneping.go +++ b/pkg/connector/phoneping.go @@ -37,13 +37,6 @@ func (wa *WhatsAppClient) FillBridgeState(state status.BridgeState) status.Bridg state.Error = WAPhoneOffline state.UserAction = status.UserActionOpenNative } - rtu := wa.UserLogin.Metadata.(*waid.UserLoginMetadata).ReachoutTimelockUntil - if !rtu.IsZero() { - if state.Info == nil { - state.Info = make(map[string]any) - } - state.Info["reachout_timelock_until"] = rtu - } return state } diff --git a/pkg/connector/startchat.go b/pkg/connector/startchat.go index c123311..aa468cb 100644 --- a/pkg/connector/startchat.go +++ b/pkg/connector/startchat.go @@ -25,8 +25,6 @@ import ( "time" "github.com/rs/zerolog" - "go.mau.fi/util/exmaps" - "go.mau.fi/util/exsync" "go.mau.fi/util/ptr" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/types" @@ -64,13 +62,6 @@ func looksEmaily(str string) bool { 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) { if strings.HasSuffix(number, "@"+types.BotServer) || strings.HasSuffix(number, "@"+types.HiddenUserServer) { return types.ParseJID(number) @@ -85,8 +76,6 @@ func (wa *WhatsAppClient) validateIdentifer(ctx context.Context, number string) return types.EmptyJID, ErrInputLooksLikeEmail } else if wa.Client == nil || !wa.Client.IsLoggedIn() { 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 { return types.EmptyJID, fmt.Errorf("failed to check if number is on WhatsApp: %w", err) } else if len(resp) == 0 { @@ -94,7 +83,6 @@ func (wa *WhatsAppClient) validateIdentifer(ctx context.Context, number string) } else if !resp[0].IsIn { return types.EmptyJID, bridgev2.WrapRespErr(fmt.Errorf("the server said +%s is not on WhatsApp", resp[0].JID.User), mautrix.MNotFound) } else { - isOnWhatsappCache.Set(number, cacheEntry{resp[0].JID, time.Now()}) 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) { - if jid.Server == types.DefaultUserServer { - lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, jid) +func (wa *WhatsAppClient) startChatLIDToPN(ctx context.Context, jid types.JID) (types.JID, error) { + if jid.Server == types.HiddenUserServer { + pn, err := wa.GetStore().LIDs.GetPNForLID(ctx, jid) if err != nil { - return jid, fmt.Errorf("failed to get lid for phone number: %w", err) - } else if lid.IsEmpty() { - resp, err := wa.Client.GetUserInfo(ctx, []types.JID{jid}) - if err != nil { - return jid, fmt.Errorf("failed to get user info for phone number: %w", err) - } else if info, ok := resp[jid]; !ok { - return jid, fmt.Errorf("server didn't return user info for phone number") - } else if info.LID.IsEmpty() { - return jid, fmt.Errorf("server didn't return lid for phone number") - } else { - return info.LID, nil - } + return jid, fmt.Errorf("failed to get phone number for lid: %w", err) + } else if pn.IsEmpty() { + // Don't allow starting chats with LIDs for now + return jid, fmt.Errorf("phone number not found") } - return lid, nil + return pn, nil } return jid, nil } @@ -156,7 +136,7 @@ func (wa *WhatsAppClient) makeCreateChatResponse(ctx context.Context, jid, origJ func (wa *WhatsAppClient) CreateChatWithGhost(ctx context.Context, ghost *bridgev2.Ghost) (*bridgev2.CreateChatResponse, error) { origJID := waid.ParseUserID(ghost.ID) - jid, err := wa.startChatPNToLID(ctx, origJID) + jid, err := wa.startChatLIDToPN(ctx, origJID) if err != nil { return nil, err } @@ -168,7 +148,7 @@ func (wa *WhatsAppClient) ResolveIdentifier(ctx context.Context, identifier stri if err != nil { return nil, err } - jid, err := wa.startChatPNToLID(ctx, origJID) + jid, err := wa.startChatLIDToPN(ctx, origJID) if err != nil { return nil, err } @@ -208,51 +188,38 @@ func (wa *WhatsAppClient) getContactList(ctx context.Context, filter string, onl return nil, err } resp := make([]*bridgev2.ResolveIdentifierResponse, 0, len(contacts)) - addedIDs := make(exmaps.Set[types.JID]) for jid, contactInfo := range contacts { - if onlyContacts && (contactInfo.FirstName == "" && contactInfo.FullName == "") { + if onlyContacts && contactInfo.FirstName == "" { continue } if !matchesQuery(contactInfo.PushName, filter) && !matchesQuery(contactInfo.FullName, filter) && !matchesQuery(jid.User, filter) { continue } - var lid types.JID - if jid.Server == types.HiddenUserServer { - lid = jid - } else if jid.Server == types.DefaultUserServer { - lid, err = wa.GetStore().LIDs.GetLIDForPN(ctx, jid) - if err != nil { - return nil, fmt.Errorf("failed to get lid for phone number %s: %w", jid, err) - } else if !lid.IsEmpty() { - jid = lid - } - } - if !addedIDs.Add(jid) { - continue - } - var chatResp *bridgev2.CreateChatResponse - if !lid.IsEmpty() { - chatResp = &bridgev2.CreateChatResponse{PortalKey: wa.makeWAPortalKey(lid)} - } ghost, _ := wa.Main.Bridge.GetGhostByID(ctx, waid.MakeUserID(jid)) resp = append(resp, &bridgev2.ResolveIdentifierResponse{ Ghost: ghost, UserID: waid.MakeUserID(jid), UserInfo: wa.contactToUserInfo(ctx, jid, contactInfo, false), - Chat: chatResp, + Chat: &bridgev2.CreateChatResponse{PortalKey: wa.makeWAPortalKey(jid)}, }) } return resp, nil } func (wa *WhatsAppClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCreateParams) (*bridgev2.CreateChatResponse, error) { + createKey := wa.Client.GenerateMessageID() + if params.RoomID != "" { + wa.createDedup.Add(createKey) + } req := whatsmeow.ReqCreateGroup{ Name: ptr.Val(params.Name).Name, Participants: make([]types.JID, len(params.Participants)), + CreateKey: createKey, } for i, participant := range params.Participants { jid := waid.ParseUserID(participant) - jid, err := wa.startChatPNToLID(ctx, jid) + // Normalize to PN if it's a LID + jid, err := wa.startChatLIDToPN(ctx, jid) if err != nil { return nil, fmt.Errorf("failed to normalize participant %s: %w", participant, err) } diff --git a/pkg/connector/userinfo.go b/pkg/connector/userinfo.go index e827955..aa84938 100644 --- a/pkg/connector/userinfo.go +++ b/pkg/connector/userinfo.go @@ -29,8 +29,7 @@ var ResyncLoopInterval = 4 * time.Hour var ResyncJitterSeconds = 3600 func (wa *WhatsAppClient) EnqueueGhostResync(ghost *bridgev2.Ghost) { - lastSync := ghost.Metadata.(*waid.GhostMetadata).LastSync.Time - if lastSync.Add(ResyncMinInterval).After(time.Now()) { + if ghost.Metadata.(*waid.GhostMetadata).LastSync.Add(ResyncMinInterval).After(time.Now()) { return } wa.resyncQueueLock.Lock() @@ -44,7 +43,6 @@ func (wa *WhatsAppClient) EnqueueGhostResync(ghost *bridgev2.Ghost) { wa.UserLogin.Log.Debug(). Stringer("jid", jid). Str("next_resync_in", nextResyncIn). - Time("last_ghost_resync", lastSync). Msg("Enqueued resync for ghost") } wa.resyncQueueLock.Unlock() @@ -52,8 +50,7 @@ func (wa *WhatsAppClient) EnqueueGhostResync(ghost *bridgev2.Ghost) { func (wa *WhatsAppClient) EnqueuePortalResync(portal *bridgev2.Portal, allowDM bool) { jid, _ := waid.ParsePortalID(portal.ID) - lastSync := portal.Metadata.(*waid.PortalMetadata).LastSync.Time - if lastSync.Add(ResyncMinInterval).After(time.Now()) { + if portal.Metadata.(*waid.PortalMetadata).LastSync.Add(ResyncMinInterval).After(time.Now()) { return } else if !allowDM && jid.Server != types.GroupServer { return @@ -64,7 +61,6 @@ func (wa *WhatsAppClient) EnqueuePortalResync(portal *bridgev2.Portal, allowDM b wa.UserLogin.Log.Debug(). Stringer("jid", jid). Stringer("next_resync_in", time.Until(wa.nextResync)). - Time("last_portal_resync", lastSync). Msg("Enqueued resync for portal") } wa.resyncQueueLock.Unlock() @@ -361,7 +357,7 @@ func (wa *WhatsAppClient) fetchGhostAvatar(ctx context.Context, ghost *bridgev2. wrappedAvatar = &bridgev2.Avatar{ ID: networkid.AvatarID(avatar.ID), Get: func(ctx context.Context) ([]byte, error) { - return wa.Client.DownloadMediaWithOnlyPath(ctx, avatar.DirectPath) + return wa.Client.DownloadMediaWithPath(ctx, avatar.DirectPath, nil, nil, nil, 0, "", "") }, } } diff --git a/pkg/connector/wadb/message.go b/pkg/connector/wadb/message.go index 7a6c63d..f2450c1 100644 --- a/pkg/connector/wadb/message.go +++ b/pkg/connector/wadb/message.go @@ -96,12 +96,12 @@ func (mq *MessageQuery) GetBetween(ctx context.Context, loginID networkid.UserLo args := []any{mq.BridgeID, loginID, chatJID} argNum := 4 if startTime != nil { - whereClauses += fmt.Sprintf(" AND timestamp > $%d", argNum) + whereClauses += fmt.Sprintf(" AND timestamp >= $%d", argNum) args = append(args, startTime.Unix()) argNum++ } if endTime != nil { - whereClauses += fmt.Sprintf(" AND timestamp < $%d", argNum) + whereClauses += fmt.Sprintf(" AND timestamp <= $%d", argNum) args = append(args, endTime.Unix()) } @@ -116,12 +116,9 @@ func (mq *MessageQuery) GetBetween(ctx context.Context, loginID networkid.UserLo AsList() } -func (mq *MessageQuery) DeleteBetween(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID, before, after uint64) (int64, error) { - res, err := mq.Exec(ctx, deleteHistorySyncMessagesBetweenQuery, mq.BridgeID, loginID, chatJID, before, after) - if err != nil { - return 0, err - } - return res.RowsAffected() +func (mq *MessageQuery) DeleteBetween(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID, before, after uint64) error { + _, err := mq.Exec(ctx, deleteHistorySyncMessagesBetweenQuery, mq.BridgeID, loginID, chatJID, before, after) + return err } func (mq *MessageQuery) DeleteAll(ctx context.Context, loginID networkid.UserLoginID) error { @@ -129,12 +126,9 @@ func (mq *MessageQuery) DeleteAll(ctx context.Context, loginID networkid.UserLog return err } -func (mq *MessageQuery) DeleteAllInChat(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID) (int64, error) { - res, err := mq.Exec(ctx, deleteHistorySyncMessagesForPortalQuery, mq.BridgeID, loginID, chatJID) - if err != nil { - return 0, err - } - return res.RowsAffected() +func (mq *MessageQuery) DeleteAllInChat(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID) error { + _, err := mq.Exec(ctx, deleteHistorySyncMessagesForPortalQuery, mq.BridgeID, loginID, chatJID) + return err } func (mq *MessageQuery) ConversationHasMessages(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID) (exists bool, err error) { diff --git a/pkg/connector/wadb/upgrades/00-latest-schema.sql b/pkg/connector/wadb/upgrades/00-latest-schema.sql index 0f1fef9..850f5b4 100644 --- a/pkg/connector/wadb/upgrades/00-latest-schema.sql +++ b/pkg/connector/wadb/upgrades/00-latest-schema.sql @@ -1,4 +1,4 @@ --- v0 -> v10 (compatible with v3+): Latest revision +-- v0 -> v9 (compatible with v3+): Latest revision CREATE TABLE whatsapp_poll_option_id ( bridge_id TEXT NOT NULL, diff --git a/pkg/connector/wadb/upgrades/08-may-need-lid-dm-deletion.sql b/pkg/connector/wadb/upgrades/08-may-need-lid-dm-deletion.sql new file mode 100644 index 0000000..d5b4ca0 --- /dev/null +++ b/pkg/connector/wadb/upgrades/08-may-need-lid-dm-deletion.sql @@ -0,0 +1,2 @@ +-- v8 (compatible with v3+): Mark LID DMs for deletion +INSERT INTO kv_store (bridge_id, key, value) VALUES ('', 'whatsapp_lid_dms_deleted', 'false'); diff --git a/pkg/connector/wadb/upgrades/09-may-need-lid-dm-deletion-again.sql b/pkg/connector/wadb/upgrades/09-may-need-lid-dm-deletion-again.sql new file mode 100644 index 0000000..5c32e65 --- /dev/null +++ b/pkg/connector/wadb/upgrades/09-may-need-lid-dm-deletion-again.sql @@ -0,0 +1,3 @@ +-- v9 (compatible with v3+): Mark LID DMs for deletion (again) +DELETE FROM kv_store WHERE bridge_id='' AND key='whatsapp_lid_dms_deleted'; +INSERT INTO kv_store (bridge_id, key, value) VALUES ('', 'whatsapp_lid_dms_deleted', 'false'); diff --git a/pkg/connector/wadb/upgrades/10-history-sync-conversation-to-lid.sql b/pkg/connector/wadb/upgrades/10-history-sync-conversation-to-lid.sql deleted file mode 100644 index 334c597..0000000 --- a/pkg/connector/wadb/upgrades/10-history-sync-conversation-to-lid.sql +++ /dev/null @@ -1,22 +0,0 @@ --- v10 (compatible with v3+): Move history sync conversations to LIDs - --- Delete history sync conversations where a @lid conversation already exists -DELETE FROM whatsapp_history_sync_conversation -WHERE chat_jid LIKE '%@lid' AND EXISTS ( - SELECT 1 - FROM whatsapp_history_sync_conversation pnconv - WHERE pnconv.chat_jid=( - SELECT pn || '@s.whatsapp.net' - FROM whatsmeow_lid_map - WHERE lid=replace(whatsapp_history_sync_conversation.chat_jid, '@lid', '') - ) -); - --- Update all phone number conversations to lids if the lid is known -UPDATE whatsapp_history_sync_conversation -SET chat_jid=(SELECT lid || '@lid' FROM whatsmeow_lid_map WHERE pn=replace(chat_jid, '@s.whatsapp.net', '')) -WHERE chat_jid LIKE '%@s.whatsapp.net' - AND EXISTS (SELECT 1 FROM whatsmeow_lid_map WHERE pn=replace(chat_jid, '@s.whatsapp.net', '')); - --- Delete blank phone number portals -DELETE FROM portal WHERE id LIKE '%@s.whatsapp.net' AND (mxid IS NULL OR mxid='') AND room_type=''; diff --git a/pkg/connector/wadb/upgrades/upgrades.go b/pkg/connector/wadb/upgrades/upgrades.go index 5547982..22c5414 100644 --- a/pkg/connector/wadb/upgrades/upgrades.go +++ b/pkg/connector/wadb/upgrades/upgrades.go @@ -6,9 +6,11 @@ import ( "go.mau.fi/util/dbutil" ) +var Table dbutil.UpgradeTable + //go:embed *.sql var rawUpgrades embed.FS -var Table = dbutil.BuildUpgradeTable(). - WithFS(rawUpgrades). - Finish() +func init() { + Table.RegisterFS(rawUpgrades) +} diff --git a/pkg/connector/wamsgtype.go b/pkg/connector/wamsgtype.go index 0a29b82..92e7f86 100644 --- a/pkg/connector/wamsgtype.go +++ b/pkg/connector/wamsgtype.go @@ -67,11 +67,10 @@ func getMessageType(waMsg *waE2E.Message) string { return "encrypted comment" case waMsg.CommentMessage != nil: return "comment" - case waMsg.PollCreationMessage != nil || waMsg.PollCreationMessageV2 != nil || waMsg.PollCreationMessageV3 != nil || - waMsg.PollCreationMessageV5 != nil || waMsg.PollCreationMessageV6 != nil: + case waMsg.PollCreationMessage != nil || waMsg.PollCreationMessageV2 != nil || waMsg.PollCreationMessageV3 != nil: return "poll create" - case waMsg.PollCreationMessageV4 != nil: - return "poll create (v4)" + case waMsg.PollCreationMessageV4 != nil || waMsg.PollCreationMessageV5 != nil: + return "poll create (vNext)" case waMsg.PollUpdateMessage != nil: return "poll update" case waMsg.ProtocolMessage != nil: @@ -131,8 +130,8 @@ func getMessageType(waMsg *waE2E.Message) string { return "secret encrypted" case waMsg.PollResultSnapshotMessage != nil: return "poll result snapshot" - case waMsg.MessageHistoryNotice != nil: - return "message history notice" + case waMsg.MessageHistoryBundle != nil: + return "message history bundle" case waMsg.RequestPhoneNumberMessage != nil: return "request phone number" case waMsg.KeepInChatMessage != nil: @@ -153,7 +152,7 @@ func getMessageType(waMsg *waE2E.Message) string { return "chat" case waMsg.PlaceholderMessage != nil: return "placeholder" - case waMsg.SenderKeyDistributionMessage != nil, waMsg.StickerSyncRmrMessage != nil, waMsg.MessageHistoryBundle != nil: + case waMsg.SenderKeyDistributionMessage != nil, waMsg.StickerSyncRmrMessage != nil: return "ignore" default: return "unknown" diff --git a/pkg/msgconv/from-matrix.go b/pkg/msgconv/from-matrix.go index 57f9113..78b8554 100644 --- a/pkg/msgconv/from-matrix.go +++ b/pkg/msgconv/from-matrix.go @@ -19,7 +19,6 @@ package msgconv import ( "bytes" "context" - "encoding/base64" "encoding/json" "errors" "fmt" @@ -50,13 +49,7 @@ import ( "go.mau.fi/mautrix-whatsapp/pkg/waid" ) -func (mc *MessageConverter) generateContextInfo( - ctx context.Context, - replyTo *database.Message, - portal *bridgev2.Portal, - perMessageTimer *event.BeeperDisappearingTimer, - roomMention bool, -) *waE2E.ContextInfo { +func (mc *MessageConverter) generateContextInfo(ctx context.Context, replyTo *database.Message, portal *bridgev2.Portal, perMessageTimer *event.BeeperDisappearingTimer) *waE2E.ContextInfo { contextInfo := &waE2E.ContextInfo{} if replyTo != nil { msgID, err := waid.ParseMessageID(replyTo.ID) @@ -64,7 +57,6 @@ func (mc *MessageConverter) generateContextInfo( contextInfo.StanzaID = proto.String(msgID.ID) contextInfo.Participant = proto.String(msgID.Sender.String()) contextInfo.QuotedMessage = &waE2E.Message{Conversation: proto.String("")} - contextInfo.QuotedType = waE2E.ContextInfo_EXPLICIT.Enum() } else { zerolog.Ctx(ctx).Warn().Err(err). Stringer("reply_to_event_id", replyTo.MXID). @@ -85,9 +77,6 @@ func (mc *MessageConverter) generateContextInfo( if setAt > 0 && contextInfo.Expiration != nil { contextInfo.EphemeralSettingTimestamp = ptr.Ptr(setAt) } - if roomMention { - contextInfo.NonJIDMentions = proto.Uint32(1) - } return contextInfo } @@ -107,7 +96,7 @@ func (mc *MessageConverter) ToWhatsApp( } message := &waE2E.Message{} - contextInfo := mc.generateContextInfo(ctx, replyTo, portal, content.BeeperDisappearingTimer, content.Mentions != nil && content.Mentions.Room) + contextInfo := mc.generateContextInfo(ctx, replyTo, portal, content.BeeperDisappearingTimer) switch content.MsgType { case event.MsgText, event.MsgNotice, event.MsgEmote: @@ -143,14 +132,14 @@ func (mc *MessageConverter) ToWhatsApp( if err != nil { 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) if err != nil { return nil, nil, fmt.Errorf("failed to encrypt comment: %w", err) } lid := parsedID.Sender if lid.Server == types.DefaultUserServer { - lid, err = client.Store.LIDs.GetLIDForPN(ctx, lid) + lid, err = client.Store.LIDs.GetLIDForPN(ctx, parsedID.Sender) if err != nil { return nil, nil, fmt.Errorf("failed to get LID for PN: %w", err) } @@ -202,7 +191,6 @@ func (mc *MessageConverter) constructMediaMessage( FileSHA256: uploaded.FileSHA256, FileLength: proto.Uint64(uploaded.FileLength), URL: proto.String(uploaded.URL), - IsLottie: proto.Bool(mime == "application/was"), }, } case event.MsgAudio: @@ -484,17 +472,6 @@ func (mc *MessageConverter) convertToWebP(img []byte) ([]byte, int, error) { return webpBuffer.Bytes(), size, nil } -func (mc *MessageConverter) getOriginalBridgedSticker(ctx context.Context, info *event.BridgedSticker) (*types.StickerPackItem, error) { - if info == nil || info.Network != StickerSourceID || !strings.HasPrefix(info.PackURL, StickerPackURLPrefix) || info.ID == "" { - return nil, nil - } - fileHash, err := base64.StdEncoding.DecodeString(info.ID) - if err != nil { - return nil, nil - } - return mc.GetCachedSticker(ctx, getClient(ctx), strings.TrimPrefix(info.PackURL, StickerPackURLPrefix), fileHash) -} - func (mc *MessageConverter) reuploadFileToWhatsApp( ctx context.Context, content *event.MessageEventContent, ) (*whatsmeow.UploadResponse, []byte, string, error) { @@ -503,25 +480,7 @@ func (mc *MessageConverter) reuploadFileToWhatsApp( if content.FileName != "" { fileName = content.FileName } - var data []byte - var err error - var sticker *types.StickerPackItem - if sticker, err = mc.getOriginalBridgedSticker(ctx, content.Info.BridgedSticker); err != nil { - zerolog.Ctx(ctx).Warn().Err(err). - Msg("Failed to get original bridged sticker, falling back to downloading from URL") - data, err = mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File) - } else if sticker != nil { - if sticker.MimeType == "application/was" { - data, err = getClient(ctx).Download(ctx, sticker) - mime = sticker.MimeType - } else { - data, err = mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File) - } - content.Info.Width = sticker.Width - content.Info.Height = sticker.Height - } else { - data, err = mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File) - } + data, err := mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File) if err != nil { return nil, nil, "", fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err) } @@ -539,14 +498,7 @@ func (mc *MessageConverter) reuploadFileToWhatsApp( case event.MessageType(event.EventSticker.Type): isSticker = true mediaType = whatsmeow.MediaImage - if mime == "video/lottie+json" { - // This likely won't work - data, err = PackAnimatedSticker(data) - if err != nil { - return nil, nil, mime, fmt.Errorf("%w (packing animated sticker): %w", bridgev2.ErrMediaConvertFailed, err) - } - mime = "application/was" - } else if (mime != "image/webp" || content.Info.Width != content.Info.Height) && mime != "application/was" { + if mime != "image/webp" || content.Info.Width != content.Info.Height { var size int data, size, err = mc.convertToWebP(data) if err != nil { diff --git a/pkg/msgconv/from-whatsapp.go b/pkg/msgconv/from-whatsapp.go index e912732..c1f36ce 100644 --- a/pkg/msgconv/from-whatsapp.go +++ b/pkg/msgconv/from-whatsapp.go @@ -140,9 +140,6 @@ func (mc *MessageConverter) ToMatrix( isBackfill bool, previouslyConvertedPart *bridgev2.ConvertedMessagePart, ) *bridgev2.ConvertedMessage { - if waMsg == nil { - waMsg = &waE2E.Message{} - } ctx = context.WithValue(ctx, contextKeyClient, client) ctx = context.WithValue(ctx, contextKeyIntent, intent) ctx = context.WithValue(ctx, contextKeyPortal, portal) @@ -175,12 +172,6 @@ func (mc *MessageConverter) ToMatrix( part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV2) case waMsg.PollCreationMessageV3 != nil: 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: part, contextInfo = mc.convertPollUpdateMessage(ctx, info, waMsg.PollUpdateMessage) case waMsg.EventMessage != nil: @@ -221,8 +212,6 @@ func (mc *MessageConverter) ToMatrix( part, contextInfo = mc.convertPlaceholderMessage(ctx, waMsg) case waMsg.GroupInviteMessage != nil: part, contextInfo = mc.convertGroupInviteMessage(ctx, info, waMsg.GroupInviteMessage) - case waMsg.MessageHistoryNotice != nil: - part, contextInfo = mc.convertMessageHistoryNotice(ctx, info, waMsg.MessageHistoryNotice) case waMsg.ProtocolMessage != nil && waMsg.ProtocolMessage.GetType() == waE2E.ProtocolMessage_EPHEMERAL_SETTING: part, contextInfo = mc.convertEphemeralSettingMessage(ctx, waMsg.ProtocolMessage, info.Timestamp, isBackfill) case waMsg.EncCommentMessage != nil: @@ -245,9 +234,6 @@ func (mc *MessageConverter) ToMatrix( part.Extra["fi.mau.whatsapp.source_broadcast_list"] = info.Chat.String() } mc.addMentions(ctx, contextInfo.GetMentionedJID(), part.Content) - if contextInfo.GetNonJIDMentions() == 1 { - part.Content.Mentions.Room = true - } cm := &bridgev2.ConvertedMessage{ Parts: []*bridgev2.ConvertedMessagePart{part}, @@ -271,26 +257,30 @@ func (mc *MessageConverter) ToMatrix( if chat.IsEmpty() { chat, _ = waid.ParsePortalID(portal.ID) } + // We reroute all DMs to the phone number JID, so reroute reply participants too + pcp = rerouteMessageKey(ctx, chat, pcp, getPortal(ctx).Metadata.(*waid.PortalMetadata).AddressingMode == types.AddressingModeLID) + if store := getClient(ctx).Store; store != nil && chat.Server == types.DefaultUserServer && pcp.Server == types.HiddenUserServer { + pcpPN, _ := store.LIDs.GetPNForLID(ctx, pcp) + zerolog.Ctx(ctx).Debug(). + Stringer("orig_participant", pcp). + Stringer("rerouted_participant", pcpPN). + Msg("Rerouting reply target (PN recipient in LID DM)") + if !pcpPN.IsEmpty() { + pcp = pcpPN + } + } else if store != nil && chat.Server == types.GroupServer && pcp.Server == types.DefaultUserServer && getPortal(ctx).Metadata.(*waid.PortalMetadata).AddressingMode == types.AddressingModeLID { + pcpLID, _ := store.LIDs.GetLIDForPN(ctx, pcp) + zerolog.Ctx(ctx).Debug(). + Stringer("orig_participant", pcp). + Stringer("rerouted_participant", pcpLID). + Msg("Rerouting reply target (PN recipient in LID group)") + if !pcpLID.IsEmpty() { + pcp = pcpLID + } + } cm.ReplyTo = &networkid.MessageOptionalPartID{ MessageID: waid.MakeMessageID(chat, pcp, contextInfo.GetStanzaID()), } - var pn, lid types.JID - if pcp.Server == types.DefaultUserServer { - pn = pcp - lid, _ = client.Store.LIDs.GetLIDForPN(ctx, pcp) - } else if pcp.Server == types.HiddenUserServer { - lid = pcp - pn, _ = client.Store.LIDs.GetPNForLID(ctx, pcp) - } else if pcp.Server == types.BotServer { - lid = pcp - } - if !pn.IsEmpty() { - cm.ReplyToLogin = waid.MakeUserLoginID(pn) - } - if !lid.IsEmpty() { - cm.ReplyToUser = waid.MakeUserID(lid) - } - // TODO set reply to room } if contextInfo.GetIsForwarded() { hasCaption := part.Content.FileName != "" && part.Content.FileName != part.Content.Body diff --git a/pkg/msgconv/matrixpoll.go b/pkg/msgconv/matrixpoll.go index a5f02ea..22cc7af 100644 --- a/pkg/msgconv/matrixpoll.go +++ b/pkg/msgconv/matrixpoll.go @@ -71,7 +71,7 @@ func (mc *MessageConverter) PollStartToWhatsApp( if maxAnswers >= len(content.PollStart.Answers) || maxAnswers < 0 { maxAnswers = 0 } - contextInfo := mc.generateContextInfo(ctx, replyTo, portal, nil, content.Mentions != nil && content.Mentions.Room) + contextInfo := mc.generateContextInfo(ctx, replyTo, portal, nil) var question string question, contextInfo.MentionedJID = mc.msc1767ToWhatsApp(ctx, content.PollStart.Question, content.Mentions) if len(question) == 0 { @@ -115,7 +115,7 @@ func (mc *MessageConverter) PollVoteToWhatsApp( zerolog.Ctx(ctx).Err(err).Msg("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" optionHashes := make([][]byte, 0, len(content.Response.Answers)) if pollMsg.Metadata.(*waid.MessageMetadata).IsMatrixPoll { @@ -146,23 +146,13 @@ func (mc *MessageConverter) PollVoteToWhatsApp( return &waE2E.Message{PollUpdateMessage: pollUpdate}, err } -func MessageIDToInfo(ctx context.Context, 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 - } - } +func MessageIDToInfo(client *whatsmeow.Client, parsedMsgID *waid.ParsedMessageID) *types.MessageInfo { return &types.MessageInfo{ MessageSource: types.MessageSource{ - Chat: chat, - Sender: sender, - IsFromMe: sender.User == client.Store.GetLID().User || sender.User == client.Store.GetJID().User, - IsGroup: chat.Server == types.GroupServer, + Chat: parsedMsgID.Chat, + Sender: parsedMsgID.Sender, + IsFromMe: parsedMsgID.Sender.User == client.Store.GetLID().User || parsedMsgID.Sender.User == client.Store.GetJID().User, + IsGroup: parsedMsgID.Chat.Server == types.GroupServer, }, ID: parsedMsgID.ID, } diff --git a/pkg/msgconv/msgconv.go b/pkg/msgconv/msgconv.go index 185ee0f..e4109c8 100644 --- a/pkg/msgconv/msgconv.go +++ b/pkg/msgconv/msgconv.go @@ -17,9 +17,6 @@ package msgconv import ( - "sync" - - "go.mau.fi/whatsmeow/types" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/format" @@ -46,16 +43,12 @@ type MessageConverter struct { DisableViewOnce bool DirectMedia bool OldMediaSuffix string - - stickerPackCache map[string]*types.StickerPack - stickerPackCacheLock sync.Mutex } func New(br *bridgev2.Bridge) *MessageConverter { mc := &MessageConverter{ - Bridge: br, - MaxFileSize: 50 * 1024 * 1024, - stickerPackCache: make(map[string]*types.StickerPack), + Bridge: br, + MaxFileSize: 50 * 1024 * 1024, } mc.HTMLParser = &format.HTMLParser{ PillConverter: mc.convertPill, diff --git a/pkg/msgconv/wa-business.go b/pkg/msgconv/wa-business.go index da416d5..c3ed976 100644 --- a/pkg/msgconv/wa-business.go +++ b/pkg/msgconv/wa-business.go @@ -66,10 +66,10 @@ func (mc *MessageConverter) convertTemplateMessage(ctx context.Context, info *ty if addButtonText { description += "\nUse the WhatsApp app to click buttons" } - content = strings.TrimSpace(fmt.Sprintf("%s\n\n%s", content, description)) + content = fmt.Sprintf("%s\n\n%s", content, description) } if footer := tpl.GetHydratedFooterText(); footer != "" { - content = strings.TrimSpace(fmt.Sprintf("%s\n\n%s", content, footer)) + content = fmt.Sprintf("%s\n\n%s", content, footer) } var convertedTitle *bridgev2.ConvertedMessagePart @@ -239,7 +239,7 @@ func (mc *MessageConverter) postProcessBusinessMessage(content string, headerMed converted.Content.Body += content contentHTML := parseWAFormattingToHTML(content, true) if contentHTML != event.TextToHTML(content) || converted.Content.FormattedBody != "" { - converted.Content.Format = event.FormatHTML + converted.Content.EnsureHasHTML() if converted.Content.FormattedBody != "" { converted.Content.FormattedBody += "

" } diff --git a/pkg/msgconv/wa-location.go b/pkg/msgconv/wa-location.go index 4a1e043..17efecd 100644 --- a/pkg/msgconv/wa-location.go +++ b/pkg/msgconv/wa-location.go @@ -20,7 +20,6 @@ import ( "bytes" "context" "fmt" - "html" "image" "math" "net/http" @@ -52,7 +51,7 @@ func (mc *MessageConverter) convertLocationMessage(ctx context.Context, msg *waE MsgType: event.MsgLocation, Body: fmt.Sprintf("Location: %s\n%s\n%s", name, msg.GetAddress(), url), Format: event.FormatHTML, - FormattedBody: fmt.Sprintf(`Location: %s
%s`, html.EscapeString(url), html.EscapeString(name), html.EscapeString(msg.GetAddress())), + FormattedBody: fmt.Sprintf("Location: %s
%s", url, name, msg.GetAddress()), GeoURI: fmt.Sprintf("geo:%.5f,%.5f", msg.GetDegreesLatitude(), msg.GetDegreesLongitude()), } diff --git a/pkg/msgconv/wa-media.go b/pkg/msgconv/wa-media.go index 9964800..9a1ceb3 100644 --- a/pkg/msgconv/wa-media.go +++ b/pkg/msgconv/wa-media.go @@ -17,6 +17,8 @@ package msgconv import ( + "archive/zip" + "bytes" "context" "encoding/json" "errors" @@ -24,19 +26,21 @@ import ( "io" "net/http" "os" + "path/filepath" + "strconv" "strings" "github.com/rs/zerolog" - "go.mau.fi/util/exfmt" "go.mau.fi/util/exmime" "go.mau.fi/util/exslices" + "go.mau.fi/util/lottie" + "go.mau.fi/util/random" "go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow/proto/waE2E" "go.mau.fi/whatsmeow/types" "maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2/database" "maunium.net/go/mautrix/event" - "maunium.net/go/mautrix/id" "go.mau.fi/mautrix-whatsapp/pkg/waid" ) @@ -83,15 +87,15 @@ func (mc *MessageConverter) convertMediaMessage( MimeType: msg.GetMimetype(), } if mc.DirectMedia { + preparedMedia.FillFileName() if preparedMedia.Info.MimeType == "application/was" { preparedMedia.Info.MimeType = "video/lottie+json" preparedMedia.FileName = "sticker.json" } - preparedMedia.FillFileName() var err error portal := getPortal(ctx) idOverride := getEditTargetID(ctx) - preparedMedia.URL, err = portal.Bridge.Matrix.GenerateContentURI(ctx, waid.MakeMediaID(messageInfo, idOverride, portal.Receiver, getMediaIDVersion(msg))) + preparedMedia.URL, err = portal.Bridge.Matrix.GenerateContentURI(ctx, waid.MakeMediaID(messageInfo, idOverride, portal.Receiver)) if err != nil { panic(fmt.Errorf("failed to generate content URI: %w", err)) } @@ -120,20 +124,13 @@ func (mc *MessageConverter) convertMediaMessage( return } -func getMediaIDVersion(msg MediaMessage) []byte { - if encSHA256 := msg.GetFileEncSHA256(); len(encSHA256) > 0 { - return encSHA256 - } - return msg.GetFileSHA256() -} - func (mc *MessageConverter) convertAlbumMessage(ctx context.Context, msg *waE2E.AlbumMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { parts := make([]string, 0, 2) if msg.GetExpectedImageCount() > 0 { - parts = append(parts, exfmt.Pluralizable("image")(int(msg.GetExpectedImageCount()))) + parts = append(parts, fmt.Sprintf("%d images", msg.GetExpectedImageCount())) } if msg.GetExpectedVideoCount() > 0 { - parts = append(parts, exfmt.Pluralizable("video")(int(msg.GetExpectedVideoCount()))) + parts = append(parts, fmt.Sprintf("%d videos", msg.GetExpectedVideoCount())) } var partDesc string if len(parts) > 0 { @@ -201,9 +198,7 @@ type PreparedMedia struct { } func (pm *PreparedMedia) FillFileName() *PreparedMedia { - if pm.Type == event.EventSticker { - pm.FileName = "" - } else if pm.FileName == "" { + if pm.FileName == "" { pm.FileName = strings.TrimPrefix(string(pm.MsgType), "m.") + exmime.ExtensionFromMimetype(pm.Info.MimeType) } return pm @@ -244,19 +239,6 @@ type MediaMessageWithDuration interface { const WhatsAppStickerSize = 190 -func fixStickerDimensions(info *event.FileInfo) { - if info.Width == info.Height { - info.Width = WhatsAppStickerSize - info.Height = WhatsAppStickerSize - } else if info.Width > info.Height { - info.Height /= info.Width / WhatsAppStickerSize - info.Width = WhatsAppStickerSize - } else { - info.Width /= info.Height / WhatsAppStickerSize - info.Height = WhatsAppStickerSize - } -} - func prepareMediaMessage(rawMsg MediaMessage) *PreparedMedia { extraInfo := map[string]any{} data := &PreparedMedia{ @@ -305,7 +287,19 @@ func prepareMediaMessage(rawMsg MediaMessage) *PreparedMedia { case *waE2E.StickerMessage: data.Type = event.EventSticker data.FileName = "sticker" + exmime.ExtensionFromMimetype(msg.GetMimetype()) - fixStickerDimensions(data.Info) + if msg.GetMimetype() == "application/was" && data.FileName == "sticker" { + data.FileName = "sticker.json" + } + if data.Info.Width == data.Info.Height { + data.Info.Width = WhatsAppStickerSize + data.Info.Height = WhatsAppStickerSize + } else if data.Info.Width > data.Info.Height { + data.Info.Height /= data.Info.Width / WhatsAppStickerSize + data.Info.Width = WhatsAppStickerSize + } else { + data.Info.Width /= data.Info.Height / WhatsAppStickerSize + data.Info.Height = WhatsAppStickerSize + } case *waE2E.VideoMessage: data.MsgType = event.MsgVideo pairedMediaType := msg.GetContextInfo().GetPairedMediaType() @@ -365,17 +359,14 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment( ) error { client := getClient(ctx) intent := getIntent(ctx) - var roomID id.RoomID - if portal := getPortal(ctx); portal != nil { - roomID = portal.MXID - } + portal := getPortal(ctx) var thumbnailData []byte var thumbnailInfo *event.FileInfo if part.Info.Size > uploadFileThreshold { var err error - part.URL, part.File, err = intent.UploadMediaStream(ctx, roomID, -1, true, func(file io.Writer) (*bridgev2.FileStreamResult, error) { + part.URL, part.File, err = intent.UploadMediaStream(ctx, portal.MXID, -1, true, func(file io.Writer) (*bridgev2.FileStreamResult, error) { err := client.DownloadToFile(ctx, message, file.(*os.File)) - if errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) { + if errors.Is(err, whatsmeow.ErrFileLengthMismatch) || errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) { zerolog.Ctx(ctx).Warn().Err(err).Msg("Mismatching media checksums in message. Ignoring because WhatsApp seems to ignore them too") } else if err != nil { return nil, fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err) @@ -396,7 +387,7 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment( } } else { data, err := client.Download(ctx, message) - if errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) { + if errors.Is(err, whatsmeow.ErrFileLengthMismatch) || errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) { zerolog.Ctx(ctx).Warn().Err(err).Msg("Mismatching media checksums in message. Ignoring because WhatsApp seems to ignore them too") } else if err != nil { return fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err) @@ -406,14 +397,12 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment( if err != nil { return err } - } else if part.Type == event.EventSticker && part.Info.MimeType == "image/webp" { - mc.fillWebPStickerInfo(ctx, part, data) } if part.Info.MimeType == "" { part.Info.MimeType = http.DetectContentType(data) } part.FillFileName() - part.URL, part.File, err = intent.UploadMedia(ctx, roomID, data, part.FileName, part.Info.MimeType) + part.URL, part.File, err = intent.UploadMedia(ctx, portal.MXID, data, part.FileName, part.Info.MimeType) if err != nil { return fmt.Errorf("%w: %w", bridgev2.ErrMediaReuploadFailed, err) } @@ -422,7 +411,7 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment( var err error part.Info.ThumbnailURL, part.Info.ThumbnailFile, err = intent.UploadMedia( ctx, - roomID, + portal.MXID, thumbnailData, "thumbnail"+exmime.ExtensionFromMimetype(thumbnailInfo.MimeType), thumbnailInfo.MimeType, @@ -436,6 +425,68 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment( return nil } +func (mc *MessageConverter) extractAnimatedSticker(fileInfo *PreparedMedia, data []byte) ([]byte, error) { + data, err := ExtractAnimatedSticker(data) + if err != nil { + return nil, err + } + fileInfo.Info.MimeType = "video/lottie+json" + fileInfo.FileName = "sticker.json" + return data, nil +} + +func (mc *MessageConverter) convertAnimatedSticker(ctx context.Context, fileInfo *PreparedMedia, data []byte) ([]byte, []byte, *event.FileInfo, error) { + data, err := mc.extractAnimatedSticker(fileInfo, data) + if err != nil { + return nil, nil, nil, err + } + c := mc.AnimatedStickerConfig + if c.Target == "disable" { + return data, nil, nil, nil + } else if !lottie.Supported() { + zerolog.Ctx(ctx).Warn().Msg("Animated sticker conversion is enabled, but lottieconverter is not installed") + return data, nil, nil, nil + } + input := bytes.NewReader(data) + fileInfo.Info.MimeType = "image/" + c.Target + fileInfo.FileName = "sticker." + c.Target + switch c.Target { + case "png": + var output bytes.Buffer + err = lottie.Convert(ctx, input, "", &output, c.Target, c.Args.Width, c.Args.Height, "1") + return output.Bytes(), nil, nil, err + case "gif": + var output bytes.Buffer + err = lottie.Convert(ctx, input, "", &output, c.Target, c.Args.Width, c.Args.Height, strconv.Itoa(c.Args.FPS)) + return output.Bytes(), nil, nil, err + case "webm", "webp": + tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("mautrix-whatsapp-lottieconverter-%s.%s", random.String(10), c.Target)) + defer func() { + _ = os.Remove(tmpFile) + }() + thumbnailData, err := lottie.FFmpegConvert(ctx, input, tmpFile, c.Args.Width, c.Args.Height, c.Args.FPS) + if err != nil { + return nil, nil, nil, err + } + data, err = os.ReadFile(tmpFile) + if err != nil { + return nil, nil, nil, fmt.Errorf("failed to read converted file: %w", err) + } + var thumbnailInfo *event.FileInfo + if thumbnailData != nil { + thumbnailInfo = &event.FileInfo{ + MimeType: "image/png", + Width: c.Args.Width, + Height: c.Args.Height, + Size: len(thumbnailData), + } + } + return data, thumbnailData, thumbnailInfo, nil + default: + return nil, nil, nil, fmt.Errorf("unsupported target format %s", c.Target) + } +} + func (mc *MessageConverter) makeMediaFailure(ctx context.Context, mediaInfo *PreparedMedia, keys *FailedMediaKeys, err error) *bridgev2.ConvertedMessagePart { logLevel := zerolog.ErrorLevel var extra map[string]any @@ -480,3 +531,28 @@ func (mc *MessageConverter) makeMediaFailure(ctx context.Context, mediaInfo *Pre } return part } + +func ExtractAnimatedSticker(data []byte) ([]byte, error) { + zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) + if err != nil { + return nil, fmt.Errorf("failed to read sticker zip: %w", err) + } + animationFile, err := zipReader.Open("animation/animation.json") + if err != nil { + return nil, fmt.Errorf("failed to open animation.json: %w", err) + } + animationFileInfo, err := animationFile.Stat() + if err != nil { + _ = animationFile.Close() + return nil, fmt.Errorf("failed to stat animation.json: %w", err) + } else if animationFileInfo.Size() > uploadFileThreshold { + _ = animationFile.Close() + return nil, fmt.Errorf("animation.json is too large (%.2f MiB)", float64(animationFileInfo.Size())/1024/1024) + } + data, err = io.ReadAll(animationFile) + _ = animationFile.Close() + if err != nil { + return nil, fmt.Errorf("failed to read animation.json: %w", err) + } + return data, nil +} diff --git a/pkg/msgconv/wa-misc.go b/pkg/msgconv/wa-misc.go index fa495e0..8dcdfb9 100644 --- a/pkg/msgconv/wa-misc.go +++ b/pkg/msgconv/wa-misc.go @@ -20,16 +20,14 @@ import ( "context" "encoding/base64" "fmt" - "html" "html/template" "strings" "time" "github.com/rs/zerolog" "go.mau.fi/util/exerrors" - "go.mau.fi/util/exfmt" "go.mau.fi/util/ptr" - "go.mau.fi/whatsmeow/proto/waAICommonDeprecated" + "go.mau.fi/whatsmeow/proto/waAICommon" "go.mau.fi/whatsmeow/proto/waE2E" "go.mau.fi/whatsmeow/types" "google.golang.org/protobuf/proto" @@ -102,7 +100,7 @@ func (mc *MessageConverter) convertGroupInviteMessage(ctx context.Context, info } } - htmlMessage := fmt.Sprintf(template, event.TextToHTML(msg.GetCaption()), html.EscapeString(msg.GetGroupName()), expiry, mc.Bridge.Config.CommandPrefix) + htmlMessage := fmt.Sprintf(template, event.TextToHTML(msg.GetCaption()), msg.GetGroupName(), expiry, mc.Bridge.Config.CommandPrefix) content := &event.MessageEventContent{ MsgType: event.MsgText, Body: format.HTMLToText(htmlMessage), @@ -119,92 +117,6 @@ func (mc *MessageConverter) convertGroupInviteMessage(ctx context.Context, info }, msg.GetContextInfo() } -func (mc *MessageConverter) formatMessageHistoryNoticeJID(ctx context.Context, jid types.JID) string { - _, displayName, err := mc.getBasicUserInfo(ctx, jid) - if err != nil { - zerolog.Ctx(ctx).Err(err).Stringer("jid", jid).Msg("Failed to get user info for message history notice") - } else if displayName != "" { - return displayName - } - switch jid.Server { - case types.DefaultUserServer: - return "+" + jid.User - default: - return "Unknown user " + jid.String() - } -} - -const maxMessageHistoryNoticeReceivers = 5 - -var others = exfmt.Pluralizable("other") - -func (mc *MessageConverter) formatMessageHistoryNoticeReceivers(ctx context.Context, receivers []string) string { - receiverLimit := min(len(receivers), maxMessageHistoryNoticeReceivers) - receiverNames := make([]string, 0, receiverLimit) - for _, receiver := range receivers[:receiverLimit] { - jid, err := types.ParseJID(receiver) - if err != nil { - zerolog.Ctx(ctx).Err(err).Str("receiver", receiver).Msg("Failed to parse message history receiver JID") - receiverNames = append(receiverNames, receiver) - } else { - receiverNames = append(receiverNames, mc.formatMessageHistoryNoticeJID(ctx, jid)) - } - } - receiverText := strings.Join(receiverNames, ", ") - if len(receivers) > receiverLimit { - receiverText = fmt.Sprintf("%s + %s", receiverText, others(len(receivers)-receiverLimit)) - } - return receiverText -} - -func (mc *MessageConverter) messageHistoryNoticeLocation(ctx context.Context) *time.Location { - portal := getPortal(ctx) - loginID := portal.Receiver - if loginID == "" { - loginID = waid.MakeUserLoginID(getClient(ctx).Store.GetJID().ToNonAD()) - } - if login := mc.Bridge.GetCachedUserLoginByID(loginID); login != nil { - meta, _ := login.Metadata.(*waid.UserLoginMetadata) - loc, err := meta.LoadTimezone() - if err != nil { - zerolog.Ctx(ctx).Err(err).Str("timezone", meta.Timezone).Msg("Failed to load user timezone for message history notice") - } else if loc != nil { - return loc - } - } - return time.Local -} - -func (mc *MessageConverter) convertMessageHistoryNotice(ctx context.Context, info *types.MessageInfo, msg *waE2E.MessageHistoryNotice) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { - metadata := msg.GetMessageHistoryMetadata() - - sender := mc.formatMessageHistoryNoticeJID(ctx, info.Sender) - body := fmt.Sprintf("%s sent message history", sender) - if receiverText := mc.formatMessageHistoryNoticeReceivers(ctx, metadata.GetHistoryReceivers()); receiverText != "" { - body = fmt.Sprintf("%s to %s", body, receiverText) - } - if count := metadata.GetMessageCount(); count > 0 { - messageWord := "messages" - if count == 1 { - messageWord = "message" - } - body = fmt.Sprintf("%s (%d %s)", body, count, messageWord) - } - if metadata != nil && metadata.OldestMessageTimestampInWindow != nil { - oldestTS := time.Unix(metadata.GetOldestMessageTimestampInWindow(), 0).In(mc.messageHistoryNoticeLocation(ctx)) - body = fmt.Sprintf("%s, starting %s", body, oldestTS.Format("Jan 2, 2006 at 3:04 PM")) - } - body += "." - - return &bridgev2.ConvertedMessagePart{ - Type: event.EventMessage, - Content: &event.MessageEventContent{ - MsgType: event.MsgNotice, - Body: body, - }, - }, msg.GetContextInfo() -} - func (mc *MessageConverter) convertEphemeralSettingMessage(ctx context.Context, msg *waE2E.ProtocolMessage, ts time.Time, isBackfill bool) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { portal := getPortal(ctx) portalMeta := portal.Metadata.(*waid.PortalMetadata) @@ -354,9 +266,8 @@ func (mc *MessageConverter) convertKeepInChatMessage(ctx context.Context, msg *w func (mc *MessageConverter) convertRichResponseMessage(ctx context.Context, msg *waE2E.AIRichResponseMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) { var body strings.Builder - // TODO switch to new format? for i, submsg := range msg.GetSubmessages() { - if submsg.GetMessageType() == waAICommonDeprecated.AIRichResponseSubMessageType_AI_RICH_RESPONSE_TEXT { + if submsg.GetMessageType() == waAICommon.AIRichResponseSubMessageType_AI_RICH_RESPONSE_TEXT { if i > 0 { body.WriteString("\n") } diff --git a/pkg/msgconv/wa-poll.go b/pkg/msgconv/wa-poll.go index 881a5fb..6dac2d6 100644 --- a/pkg/msgconv/wa-poll.go +++ b/pkg/msgconv/wa-poll.go @@ -94,7 +94,31 @@ func (mc *MessageConverter) convertPollCreationMessage(ctx context.Context, msg }, msg.GetContextInfo() } +func rerouteMessageKey(ctx context.Context, chat, sender types.JID, groupLIDAddressing bool) types.JID { + if store := getClient(ctx).Store; store != nil && chat.Server == types.DefaultUserServer && sender.Server == types.HiddenUserServer { + senderPN, _ := store.LIDs.GetPNForLID(ctx, sender) + zerolog.Ctx(ctx).Debug(). + Stringer("orig_participant", sender). + Stringer("rerouted_participant", senderPN). + Msg("Rerouting message key (PN recipient in LID DM)") + if !senderPN.IsEmpty() { + return senderPN + } + } else if store != nil && chat.Server == types.GroupServer && sender.Server == types.DefaultUserServer && groupLIDAddressing { + senderLID, _ := store.LIDs.GetLIDForPN(ctx, sender) + zerolog.Ctx(ctx).Debug(). + Stringer("orig_participant", sender). + Stringer("rerouted_participant", senderLID). + Msg("Rerouting message key (PN recipient in LID group)") + if !senderLID.IsEmpty() { + return senderLID + } + } + return sender +} + func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender types.JID, key *waCommon.MessageKey) networkid.MessageID { + groupLIDAddressing := sender.Server == types.HiddenUserServer sender = sender.ToNonAD() var err error if !key.GetFromMe() { @@ -107,7 +131,7 @@ func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender if sender.Server == types.LegacyUserServer { sender.Server = types.DefaultUserServer } - } else if chat.Server == types.DefaultUserServer || chat.Server == types.HiddenUserServer || chat.Server == types.BotServer { + } else if chat.Server == types.DefaultUserServer || chat.Server == types.BotServer { if sender.User == client.Store.GetJID().User || sender.User == client.Store.GetLID().User { // Message key is not from the sender, but message sender (containing key) is me, // so message key sender is the other user in the DM @@ -115,11 +139,7 @@ func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender } else { // Message key is not from the sender, but message sender (containing key) is not me, // so message key sender is me - if chat.Server == types.HiddenUserServer { - sender = client.Store.GetLID().ToNonAD() - } else { - sender = client.Store.GetJID().ToNonAD() - } + sender = client.Store.GetJID().ToNonAD() } } else { zerolog.Ctx(ctx).Warn(). @@ -137,6 +157,10 @@ func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender chat = remoteJID } } + sender = rerouteMessageKey( + context.WithValue(ctx, contextKeyClient, client), + chat, sender, groupLIDAddressing, + ) return waid.MakeMessageID(chat, sender, key.GetID()) } @@ -153,9 +177,6 @@ func (mc *MessageConverter) convertPollUpdateMessage(ctx context.Context, info * if err != nil { log.Err(err).Msg("Failed to get poll update target message") return failedPollUpdatePart, nil - } else if pollMessage == nil { - log.Warn().Str("target_message_id", string(pollMessageID)).Msg("Poll update target message not found") - return failedPollUpdatePart, nil } vote, err := getClient(ctx).DecryptPollVote(ctx, &events.Message{ Info: *info, diff --git a/pkg/msgconv/wa-sticker.go b/pkg/msgconv/wa-sticker.go deleted file mode 100644 index b9797a6..0000000 --- a/pkg/msgconv/wa-sticker.go +++ /dev/null @@ -1,455 +0,0 @@ -// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge. -// Copyright (C) 2026 Tulir Asokan -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU Affero General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU Affero General Public License for more details. -// -// You should have received a copy of the GNU Affero General Public License -// along with this program. If not, see . - -package msgconv - -import ( - "archive/zip" - "bytes" - "context" - "encoding/base64" - "encoding/binary" - "encoding/json" - "errors" - "fmt" - "io" - "net/url" - "os" - "path/filepath" - "strconv" - "strings" - - "github.com/rs/zerolog" - "github.com/tidwall/gjson" - "go.mau.fi/util/exstrings" - "go.mau.fi/util/lottie" - "go.mau.fi/util/random" - "go.mau.fi/whatsmeow" - "go.mau.fi/whatsmeow/types" - "maunium.net/go/mautrix" - "maunium.net/go/mautrix/bridgev2" - "maunium.net/go/mautrix/bridgev2/database" - "maunium.net/go/mautrix/bridgev2/networkid" - "maunium.net/go/mautrix/event" - - "go.mau.fi/mautrix-whatsapp/pkg/waid" -) - -func (mc *MessageConverter) GetCachedStickerPack(ctx context.Context, client *whatsmeow.Client, packID string) (*types.StickerPack, error) { - mc.stickerPackCacheLock.Lock() - defer mc.stickerPackCacheLock.Unlock() - cached, ok := mc.stickerPackCache[packID] - if ok { - if cached == nil { - return nil, bridgev2.RespError(mautrix.MNotFound.WithMessage("sticker pack not found (cached)")) - } - return cached, nil - } - - pack, err := client.FetchStickerPack(ctx, packID) - if errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) { - mc.stickerPackCache[packID] = nil - return nil, bridgev2.WrapRespErr(err, mautrix.MNotFound) - } else if err != nil { - return nil, err - } - mc.stickerPackCache[packID] = pack - if packID != pack.StickerPackID { - mc.stickerPackCache[pack.StickerPackID] = pack - } - return pack, nil -} - -func (mc *MessageConverter) GetCachedSticker(ctx context.Context, client *whatsmeow.Client, packID string, hash []byte) (*types.StickerPackItem, error) { - pack, err := mc.GetCachedStickerPack(ctx, client, packID) - if err != nil { - return nil, err - } - for _, sticker := range pack.Stickers { - if bytes.Equal(sticker.FileHash, hash) { - return sticker, nil - } - } - return nil, nil -} - -func (mc *MessageConverter) DownloadImagePack(ctx context.Context, userLoginID networkid.UserLoginID, client *whatsmeow.Client, inputURL string) (*bridgev2.ImportedImagePack, error) { - parsedURL, err := url.Parse(inputURL) - if err != nil { - return nil, bridgev2.WrapRespErr(err, mautrix.MNotFound) - } else if parsedURL.Host != "api.whatsapp.com" && parsedURL.Host != "wa.me" { - return nil, bridgev2.WrapRespErr(fmt.Errorf("invalid host %q", parsedURL.Host), mautrix.MNotFound) - } else if !strings.HasPrefix(parsedURL.Path, "/stickerpack/") { - return nil, bridgev2.WrapRespErr(fmt.Errorf("invalid path %q", parsedURL.Path), mautrix.MNotFound) - } - packName := strings.Split(strings.TrimPrefix(parsedURL.Path, "/stickerpack/"), "/")[0] - if packName == "" { - return nil, bridgev2.WrapRespErr(fmt.Errorf("empty pack name"), mautrix.MNotFound) - } - pack, err := mc.GetCachedStickerPack(ctx, client, packName) - if err != nil { - return nil, err - } - canonicalURL := "https://wa.me/stickerpack/" + pack.StickerPackID - topLevelExtra := map[string]any{ - "fi.mau.whatsapp.stickerpack": map[string]any{ - "id": pack.StickerPackID, - "name": pack.Name, - "description": pack.Description, - "publisher": pack.Publisher, - "animated": pack.Animated > 0, - "lottie": pack.Lottie > 0, - }, - } - content := &event.ImagePackEventContent{ - Images: make(map[string]*event.ImagePackImage, len(pack.Stickers)), - Metadata: event.ImagePackMetadata{ - DisplayName: pack.Name, - AvatarURL: "", - Usage: []event.ImagePackUsage{event.ImagePackUsageSticker}, - Attribution: fmt.Sprintf("By %s on WhatsApp %s", pack.Publisher, canonicalURL), - BridgedPack: &event.BridgedStickerPack{ - Network: StickerSourceID, - URL: canonicalURL, - }, - }, - } - ctx = context.WithValue(ctx, contextKeyClient, client) - ctx = context.WithValue(ctx, contextKeyIntent, mc.Bridge.Bot) - ctx = context.WithValue(ctx, contextKeyPortal, (*bridgev2.Portal)(nil)) - for i, sticker := range pack.Stickers { - shortcode := sticker.PreviewWebpID - if shortcode == "" { - shortcode = fmt.Sprintf("%s_img%d", pack.StickerPackID, i+1) - } - body := sticker.AccessibilityText - var emoji string - if len(sticker.Emojis) > 0 { - emoji = sticker.Emojis[0] - if body == "" { - body = strings.Join(sticker.Emojis, " ") - } - } - part := &PreparedMedia{ - Type: event.EventSticker, - MessageEventContent: &event.MessageEventContent{ - Body: body, - Info: &event.FileInfo{ - MimeType: sticker.MimeType, - Width: sticker.Width, - Height: sticker.Height, - Size: int(sticker.FileSize), - BridgedSticker: &event.BridgedSticker{ - Network: StickerSourceID, - ID: base64.StdEncoding.EncodeToString(sticker.FileHash), - Emoji: emoji, - PackURL: canonicalURL, - }, - }, - }, - TypeDescription: "sticker", - } - dbKey := database.Key(fmt.Sprintf("stickercache:%x", part.Info.BridgedSticker.ID)) - fixStickerDimensions(part.Info) - var packed *event.ImagePackImage - if mc.DirectMedia { - dbKey = "" - if part.Info.MimeType == "application/was" { - part.Info.MimeType = "video/lottie+json" - } - part.URL, err = mc.Bridge.Matrix.GenerateContentURI(ctx, waid.MakeStickerPackMediaID(pack.StickerPackID, sticker.FileHash, userLoginID)) - if err != nil { - panic(fmt.Errorf("failed to generate content URI: %w", err)) - } - } else if cached := mc.Bridge.DB.KV.Get(ctx, dbKey); cached != "" { - err = json.Unmarshal([]byte(cached), &packed) - if err != nil { - return nil, fmt.Errorf("failed to unmarshal cached sticker data: %w", err) - } - } else { - err = mc.reuploadWhatsAppAttachment(ctx, sticker, part) - if err != nil { - return nil, fmt.Errorf("failed to reupload sticker %q: %w", sticker.GetDirectPath(), err) - } - } - if packed == nil { - packed = &event.ImagePackImage{ - URL: part.URL, - Body: part.Body, - Info: part.Info, - } - if dbKey != "" { - data, _ := json.Marshal(packed) - if data != nil { - mc.Bridge.DB.KV.Set(ctx, dbKey, string(data)) - } - } - } - content.Images[shortcode] = packed - } - - return &bridgev2.ImportedImagePack{ - Content: content, - Extra: topLevelExtra, - Shortcode: pack.StickerPackID, - }, nil -} - -type StickerMetadata struct { - StickerPackID string `json:"sticker-pack-id"` - AccessibilityText string `json:"accessibility-text"` - Emojis []string `json:"emojis"` - IsFirstPartySticker int `json:"is-first-party-sticker"` -} - -func (sm *StickerMetadata) ToMatrix(content *event.MessageEventContent) { - if sm == nil { - return - } - if sm.StickerPackID != "" && content.Info.BridgedSticker == nil { - content.Info.BridgedSticker = &event.BridgedSticker{ - Network: StickerSourceID, - PackURL: StickerPackURLPrefix + sm.StickerPackID, - } - if len(sm.Emojis) > 0 { - content.Info.BridgedSticker.Emoji = sm.Emojis[0] - } - } - if sm.AccessibilityText != "" { - content.Body = sm.AccessibilityText - } else if len(sm.Emojis) > 0 { - content.Body = strings.Join(sm.Emojis, " ") - } -} - -const StickerSourceID = "whatsapp" -const StickerPackURLPrefix = "https://wa.me/stickerpack/" - -func PackAnimatedSticker(data []byte) ([]byte, error) { - var buf bytes.Buffer - zipWriter := zip.NewWriter(&buf) - f, err := zipWriter.Create("animation/animation.json") - if err != nil { - return nil, fmt.Errorf("failed to create zip entry: %w", err) - } - _, err = f.Write(data) - if err != nil { - return nil, fmt.Errorf("failed to write zip entry: %w", err) - } - err = zipWriter.Close() - if err != nil { - return nil, fmt.Errorf("failed to close zip writer: %w", err) - } - return buf.Bytes(), nil -} - -func ExtractAnimatedSticker(data []byte) ([]byte, *StickerMetadata, error) { - zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data))) - if err != nil { - return nil, nil, fmt.Errorf("failed to read sticker zip: %w", err) - } - animationFile, err := zipReader.Open("animation/animation.json") - if err != nil { - return nil, nil, fmt.Errorf("failed to open animation.json: %w", err) - } - animationFileInfo, err := animationFile.Stat() - if err != nil { - _ = animationFile.Close() - return nil, nil, fmt.Errorf("failed to stat animation.json: %w", err) - } else if animationFileInfo.Size() > uploadFileThreshold { - _ = animationFile.Close() - return nil, nil, fmt.Errorf("animation.json is too large (%.2f MiB)", float64(animationFileInfo.Size())/1024/1024) - } - data, err = io.ReadAll(animationFile) - _ = animationFile.Close() - if err != nil { - return nil, nil, fmt.Errorf("failed to read animation.json: %w", err) - } - var meta StickerMetadata - metaFile, err := zipReader.Open("animation/animation.json.overridden_metadata") - if err == nil { - _ = json.NewDecoder(metaFile).Decode(&meta) - _ = metaFile.Close() - } - if meta.StickerPackID == "" { - res := gjson.GetBytes(data, "metadata.customProps") - if res.IsObject() { - _ = json.Unmarshal(exstrings.UnsafeBytes(res.Raw), &meta) - } - } - return data, &meta, nil -} - -func (mc *MessageConverter) extractAnimatedSticker(fileInfo *PreparedMedia, data []byte) ([]byte, error) { - data, meta, err := ExtractAnimatedSticker(data) - if err != nil { - return nil, err - } - meta.ToMatrix(fileInfo.MessageEventContent) - fileInfo.Info.MimeType = "video/lottie+json" - fileInfo.FileName = "sticker.json" - return data, nil -} - -func (mc *MessageConverter) convertAnimatedSticker(ctx context.Context, fileInfo *PreparedMedia, data []byte) ([]byte, []byte, *event.FileInfo, error) { - data, err := mc.extractAnimatedSticker(fileInfo, data) - if err != nil { - return nil, nil, nil, err - } - c := mc.AnimatedStickerConfig - if c.Target == "disable" { - return data, nil, nil, nil - } else if !lottie.Supported() { - zerolog.Ctx(ctx).Warn().Msg("Animated sticker conversion is enabled, but lottieconverter is not installed") - return data, nil, nil, nil - } - input := bytes.NewReader(data) - fileInfo.Info.MimeType = "image/" + c.Target - fileInfo.FileName = "sticker." + c.Target - switch c.Target { - case "png": - var output bytes.Buffer - err = lottie.Convert(ctx, input, "", &output, c.Target, c.Args.Width, c.Args.Height, "1") - return output.Bytes(), nil, nil, err - case "gif": - var output bytes.Buffer - err = lottie.Convert(ctx, input, "", &output, c.Target, c.Args.Width, c.Args.Height, strconv.Itoa(c.Args.FPS)) - return output.Bytes(), nil, nil, err - case "webm", "webp": - tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("mautrix-whatsapp-lottieconverter-%s.%s", random.String(10), c.Target)) - defer func() { - _ = os.Remove(tmpFile) - }() - thumbnailData, err := lottie.FFmpegConvert(ctx, input, tmpFile, c.Args.Width, c.Args.Height, c.Args.FPS) - if err != nil { - return nil, nil, nil, err - } - data, err = os.ReadFile(tmpFile) - if err != nil { - return nil, nil, nil, fmt.Errorf("failed to read converted file: %w", err) - } - var thumbnailInfo *event.FileInfo - if thumbnailData != nil { - thumbnailInfo = &event.FileInfo{ - MimeType: "image/png", - Width: c.Args.Width, - Height: c.Args.Height, - Size: len(thumbnailData), - } - } - return data, thumbnailData, thumbnailInfo, nil - default: - return nil, nil, nil, fmt.Errorf("unsupported target format %s", c.Target) - } -} - -func (mc *MessageConverter) fillWebPStickerInfo(ctx context.Context, fileInfo *PreparedMedia, data []byte) { - meta, err := extractWebPStickerMetadata(data) - if err != nil { - zerolog.Ctx(ctx).Debug().Err(err).Msg("Failed to extract webp sticker metadata") - return - } - meta.ToMatrix(fileInfo.MessageEventContent) -} - -// stickerMetadataEXIFTag is the custom EXIF tag WhatsApp uses to embed -// sticker pack metadata as a JSON object inside non-animated webp stickers. -const stickerMetadataEXIFTag = 0x5741 - -// extractWebPStickerMetadata parses the WhatsApp sticker pack metadata JSON -// embedded in EXIF tag 0x5741 of a non-animated webp sticker. -func extractWebPStickerMetadata(data []byte) (*StickerMetadata, error) { - exif, err := findWebPChunk(data, "EXIF") - if err != nil { - return nil, err - } - raw, err := findEXIFTagValue(exif, stickerMetadataEXIFTag) - if err != nil { - return nil, err - } - var meta StickerMetadata - err = json.Unmarshal(raw, &meta) - if err != nil { - return nil, fmt.Errorf("failed to parse sticker metadata JSON: %w", err) - } - return &meta, nil -} - -func findWebPChunk(data []byte, chunkType string) ([]byte, error) { - if len(data) < 12 || string(data[0:4]) != "RIFF" || string(data[8:12]) != "WEBP" { - return nil, fmt.Errorf("not a webp file") - } - for pos := 12; pos+8 <= len(data); { - size := binary.LittleEndian.Uint32(data[pos+4 : pos+8]) - start := pos + 8 - end := start + int(size) - if end > len(data) { - return nil, fmt.Errorf("webp chunk %q extends past end of file", data[pos:pos+4]) - } - if string(data[pos:pos+4]) == chunkType { - return data[start:end], nil - } - pos = end - if pos%2 != 0 { - pos++ - } - } - return nil, fmt.Errorf("webp chunk %q not found", chunkType) -} - -func findEXIFTagValue(exif []byte, tag uint16) ([]byte, error) { - if len(exif) < 8 { - return nil, fmt.Errorf("exif data too short") - } - var bo binary.ByteOrder - switch string(exif[0:2]) { - case "II": - bo = binary.LittleEndian - case "MM": - bo = binary.BigEndian - default: - return nil, fmt.Errorf("invalid TIFF byte order %q", exif[0:2]) - } - if bo.Uint16(exif[2:4]) != 0x002A { - return nil, fmt.Errorf("invalid TIFF magic") - } - ifdOffset := int(bo.Uint32(exif[4:8])) - if ifdOffset < 0 || ifdOffset+2 > len(exif) { - return nil, fmt.Errorf("IFD offset out of range") - } - count := int(bo.Uint16(exif[ifdOffset : ifdOffset+2])) - entries := ifdOffset + 2 - if entries+count*12 > len(exif) { - return nil, fmt.Errorf("IFD entries out of range") - } - for i := 0; i < count; i++ { - entry := exif[entries+i*12 : entries+(i+1)*12] - if bo.Uint16(entry[0:2]) != tag { - continue - } - // Tag 0x5741 stores JSON as type 7 (UNDEFINED), where size == count bytes. - size := int(bo.Uint32(entry[4:8])) - if size <= 4 { - return entry[8 : 8+size], nil - } - offset := int(bo.Uint32(entry[8:12])) - if offset+size > len(exif) { - return nil, fmt.Errorf("exif tag value out of range") - } - return exif[offset : offset+size], nil - } - return nil, fmt.Errorf("exif tag 0x%04x not found", tag) -} diff --git a/pkg/waid/dbmeta.go b/pkg/waid/dbmeta.go index 1e54426..2785105 100644 --- a/pkg/waid/dbmeta.go +++ b/pkg/waid/dbmeta.go @@ -41,8 +41,7 @@ type UserLoginMetadata struct { AppStateRecoveryAttempted map[appstate.WAPatchName]time.Time `json:"app_state_recovery_attempted,omitempty"` - HistorySyncPortalsNeedCreating bool `json:"history_sync_portals_need_creating,omitempty"` - ReachoutTimelockUntil time.Time `json:"reachout_timelock_until,omitempty"` + HistorySyncPortalsNeedCreating bool `json:"history_sync_portals_need_creating,omitempty"` MData json.RawMessage `json:"mdata,omitempty"` } @@ -62,13 +61,6 @@ func (m *UserLoginMetadata) GeneratePushKeys() { } } -func (m *UserLoginMetadata) LoadTimezone() (*time.Location, error) { - if m == nil || m.Timezone == "" { - return nil, nil - } - return time.LoadLocation(m.Timezone) -} - type MessageErrorType string const ( diff --git a/pkg/waid/id.go b/pkg/waid/id.go index 7f8fe5b..840e0f1 100644 --- a/pkg/waid/id.go +++ b/pkg/waid/id.go @@ -83,22 +83,14 @@ func MakeMessageID(chat, sender types.JID, id types.MessageID) networkid.Message return networkid.MessageID(fmt.Sprintf("%s:%s:%s", chat.ToNonAD().String(), sender.ToNonAD().String(), id)) } -func MakeMessageIDWithAltSender(chat, sender, altSender types.JID, id types.MessageID) networkid.MessageID { - if chat.Server == types.HiddenUserServer && sender.Server == types.DefaultUserServer && altSender.Server == types.HiddenUserServer { - sender = altSender - } - return MakeMessageID(chat, sender, id) -} - func MakeFakeMessageID(chat, sender types.JID, data string) networkid.MessageID { return networkid.MessageID(fmt.Sprintf("fake:%s:%s:%s", chat.ToNonAD().String(), sender.ToNonAD().String(), data)) } type ParsedMessageID struct { - Chat types.JID - Sender types.JID - ID types.MessageID - Version []byte + Chat types.JID + Sender types.JID + ID types.MessageID } func (pmi *ParsedMessageID) String() networkid.MessageID { diff --git a/pkg/waid/mediaid.go b/pkg/waid/mediaid.go index 716e187..6a94de4 100644 --- a/pkg/waid/mediaid.go +++ b/pkg/waid/mediaid.go @@ -33,10 +33,9 @@ const ( mediaIDTypeMessage = 255 mediaIDTypeAvatar = 254 mediaIDTypeCommunityAvatar = 253 - mediaIDTypeStickerPackItem = 252 ) -func MakeMediaID(messageInfo *types.MessageInfo, idOverride types.MessageID, receiver networkid.UserLoginID, version []byte) networkid.MediaID { +func MakeMediaID(messageInfo *types.MessageInfo, idOverride types.MessageID, receiver networkid.UserLoginID) networkid.MediaID { compactChat := compactJID(messageInfo.Chat.ToNonAD()) compactSender := compactJID(messageInfo.Sender.ToNonAD()) receiverID := compactJID(ParseUserLoginID(receiver, 0)) @@ -46,7 +45,7 @@ func MakeMediaID(messageInfo *types.MessageInfo, idOverride types.MessageID, rec } else { compactID = compactMsgID(messageInfo.ID) } - mediaID := make([]byte, 0, 6+len(compactChat)+len(compactSender)+len(receiverID)+len(compactID)+len(version)) + mediaID := make([]byte, 0, 5+len(compactChat)+len(compactSender)+len(receiverID)+len(compactID)) mediaID = append(mediaID, mediaIDTypeMessage) mediaID = append(mediaID, byte(len(compactChat))) mediaID = append(mediaID, compactChat...) @@ -56,8 +55,6 @@ func MakeMediaID(messageInfo *types.MessageInfo, idOverride types.MessageID, rec mediaID = append(mediaID, receiverID...) mediaID = append(mediaID, byte(len(compactID))) mediaID = append(mediaID, compactID...) - mediaID = append(mediaID, byte(len(version))) - mediaID = append(mediaID, version...) return mediaID } @@ -85,28 +82,9 @@ type AvatarMediaInfo struct { Community bool } -func MakeStickerPackMediaID(packID string, fileHash []byte, receiver networkid.UserLoginID) networkid.MediaID { - receiverID := compactJID(ParseUserLoginID(receiver, 0)) - mediaID := make([]byte, 0, 4+len(packID)+len(fileHash)+len(receiverID)) - mediaID = append(mediaID, mediaIDTypeStickerPackItem) - mediaID = append(mediaID, byte(len(packID))) - mediaID = append(mediaID, packID...) - mediaID = append(mediaID, byte(len(fileHash))) - mediaID = append(mediaID, fileHash...) - mediaID = append(mediaID, byte(len(receiverID))) - mediaID = append(mediaID, receiverID...) - return mediaID -} - -type StickerPackMediaInfo struct { - PackID string - FileHash []byte -} - type ParsedMediaID struct { Message *ParsedMessageID Avatar *AvatarMediaInfo - Sticker *StickerPackMediaInfo UserLogin networkid.UserLoginID } @@ -140,12 +118,6 @@ func ParseMediaID(mediaID networkid.MediaID) (*ParsedMediaID, error) { Sender: senderJID, ID: id, } - if len(mediaID) > 0 { - parsed.Message.Version, err = readCompact(&mediaID, rawBytes) - if err != nil { - return nil, fmt.Errorf("failed to parse version: %w", err) - } - } parsed.UserLogin = MakeUserLoginID(receiverID) case mediaIDTypeAvatar, mediaIDTypeCommunityAvatar: targetJID, err := readCompact(&mediaID, parseCompactJID) @@ -166,24 +138,6 @@ func ParseMediaID(mediaID networkid.MediaID) (*ParsedMediaID, error) { Community: mediaIDType == mediaIDTypeCommunityAvatar, } parsed.UserLogin = MakeUserLoginID(receiverID) - case mediaIDTypeStickerPackItem: - packID, err := readCompact(&mediaID, parseString) - if err != nil { - return nil, fmt.Errorf("failed to parse sticker pack ID: %w", err) - } - fileHash, err := readCompact(&mediaID, rawBytes) - if err != nil { - return nil, fmt.Errorf("failed to parse sticker file hash: %w", err) - } - receiverID, err := readCompact(&mediaID, parseCompactJID) - if err != nil { - return nil, fmt.Errorf("failed to parse receiver JID: %w", err) - } - parsed.Sticker = &StickerPackMediaInfo{ - PackID: packID, - FileHash: fileHash, - } - parsed.UserLogin = MakeUserLoginID(receiverID) default: return nil, fmt.Errorf("unknown media ID type %d", mediaIDType) } @@ -292,10 +246,6 @@ func parseCompactJID(jid []byte) (types.JID, error) { } } -func rawBytes(data []byte) ([]byte, error) { - return data, nil -} - func readCompact[T any](data *networkid.MediaID, fn func(data []byte) (T, error)) (T, error) { var defVal T if len(*data) < 1 {