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

Compare commits

..
Author SHA1 Message Date
Nick Mills-Barrett
a2f5fc0f51
handlewhatsapp: fix unintended drop when handling undecryptable message 2026-02-26 19:07:19 +00:00
49 changed files with 1007 additions and 2186 deletions

View file

@ -7,12 +7,10 @@ type: Bug
--- ---
<!-- Include relevant logs, the bridge version and other important details here --> <!--
Remember to include relevant logs, the bridge version and any other details.
### Checklist 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
<!-- All items below are mandatory. Issues not following the rules may be closed without comment. --> ignored or closed immediately.
-->
* [ ] 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: ``

View file

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

View file

@ -1,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 # v26.02
* Bumped minimum Go version to 1.25. * Bumped minimum Go version to 1.25.

View file

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

View file

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

View file

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

View file

@ -18,12 +18,21 @@ var m = mxmain.BridgeMain{
Name: "mautrix-whatsapp", Name: "mautrix-whatsapp",
URL: "https://github.com/mautrix/whatsapp", URL: "https://github.com/mautrix/whatsapp",
Description: "A Matrix-WhatsApp puppeting bridge.", Description: "A Matrix-WhatsApp puppeting bridge.",
Version: "26.08", Version: "26.02",
SemCalVer: true, SemCalVer: true,
Connector: &connector.WhatsAppConnector{}, Connector: &connector.WhatsAppConnector{},
} }
func main() { 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.InitVersion(Tag, Commit, BuildTime)
m.Run() m.Run()
} }

View file

@ -0,0 +1,24 @@
//go:build amd64 && cgo && !noplugin
package main
import (
"fmt"
"os"
"plugin"
"go.mau.fi/util/exerrors"
"go.mau.fi/mautrix-whatsapp/pkg/connector"
)
func init() {
path := os.Getenv("WM_PLUGIN_PATH")
if path == "" {
return
}
fmt.Println("Loading plugin from", path)
plug := exerrors.Must(plugin.Open(path))
sym := exerrors.Must(plug.Lookup("NewClient"))
connector.NewMC = sym.(connector.NewMCFunc)
}

50
go.mod
View file

@ -1,53 +1,53 @@
module go.mau.fi/mautrix-whatsapp module go.mau.fi/mautrix-whatsapp
go 1.26.0 go 1.25.0
toolchain go1.27.0 toolchain go1.26.0
tool go.mau.fi/util/cmd/maubuild tool go.mau.fi/util/cmd/maubuild
require ( require (
github.com/lib/pq v1.12.3 github.com/lib/pq v1.11.2
github.com/rs/zerolog v1.35.1 github.com/rs/zerolog v1.34.0
github.com/tidwall/gjson v1.19.0 go.mau.fi/util v0.9.6
go.mau.fi/util v0.10.1-0.20260820140024-eb612d936fde go.mau.fi/webp v0.2.0
go.mau.fi/webp v0.3.0 go.mau.fi/whatsmeow v0.0.0-20260218131543-e4d82a04d5d8
go.mau.fi/whatsmeow v0.0.0-20260821141805-33cfac511629 golang.org/x/image v0.36.0
golang.org/x/image v0.45.0 golang.org/x/net v0.50.0
golang.org/x/net v0.58.0 golang.org/x/sync v0.19.0
golang.org/x/sync v0.22.0 google.golang.org/protobuf v1.36.11
google.golang.org/protobuf v1.36.12
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
maunium.net/go/mautrix v0.30.1-0.20260820142923-ae00f2c62e1e maunium.net/go/mautrix v0.26.3
) )
require ( 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/beeper/argo-go v1.1.2 // indirect
github.com/coder/websocket v1.8.15 // indirect github.com/coder/websocket v1.8.14 // indirect
github.com/coreos/go-systemd/v22 v22.7.0 // indirect github.com/coreos/go-systemd/v22 v22.6.0 // indirect
github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/kr/pretty v0.3.1 // indirect github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.49 // indirect github.com/mattn/go-sqlite3 v1.14.34 // indirect
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b // indirect github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/rs/xid v1.6.0 // indirect github.com/rs/xid v1.6.0 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.2.0 // indirect github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect github.com/tidwall/sjson v1.2.5 // indirect
github.com/vektah/gqlparser/v2 v2.5.27 // indirect github.com/vektah/gqlparser/v2 v2.5.27 // indirect
github.com/yuin/goldmark v1.8.5 // indirect github.com/yuin/goldmark v1.7.16 // indirect
go.mau.fi/libsignal v0.2.2 // indirect go.mau.fi/libsignal v0.2.1 // indirect
go.mau.fi/zeroconfig v0.2.0 // indirect go.mau.fi/zeroconfig v0.2.0 // indirect
golang.org/x/crypto v0.55.0 // indirect golang.org/x/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 // indirect golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a // indirect
golang.org/x/mod v0.40.0 // indirect golang.org/x/mod v0.33.0 // indirect
golang.org/x/sys v0.47.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
maunium.net/go/mauflag v1.0.0 // indirect maunium.net/go/mauflag v1.0.0 // indirect

100
go.sum
View file

@ -1,5 +1,5 @@
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo= filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc= 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 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU= 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= 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/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 h1:UQI2G8F+NLfGTOmTUI0254pGKx/HUU/etbUGTJv91Fs=
github.com/beeper/argo-go v1.1.2/go.mod h1:M+LJAnyowKVQ6Rdj6XYGEn+qcVFkb3R/MUpqkGR0hM4= 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.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/coder/websocket v1.8.14/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.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= 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/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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo= 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 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 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.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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 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.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= 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 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.49 h1:B8jBHC3xhxZgxztrgruTuLucebnULQnx4W7cF7SAE9w= github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
github.com/mattn/go-sqlite3 v1.14.49/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b h1:sS7HLzwS+dO+gxATgQfeZDEdUZe2pKAB3nGoUwP5zU0= github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 h1:KPpdlQLZcHfTMQRi6bFQ7ogNO0ltFT4PmtwTLW4W+14=
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= 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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 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/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 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= 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.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= 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 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e 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 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= 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.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= 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.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 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/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s= github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=
github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA= github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/libsignal v0.2.2 h1:QV+XdzQkm3x3aSG7FcqfGSZuFXz83pRZPBFaPygHbOU= go.mau.fi/libsignal v0.2.1 h1:vRZG4EzTn70XY6Oh/pVKrQGuMHBkAWlGRC22/85m9L0=
go.mau.fi/libsignal v0.2.2/go.mod h1:CRlIQg2J8uYTfDFvNoO8/KcZjs5cey0vbc6oj/bssY0= go.mau.fi/libsignal v0.2.1/go.mod h1:iVvjrHyfQqWajOUaMEsIfo3IqgVMrhWcPiiEzk7NgoU=
go.mau.fi/util v0.10.1-0.20260820140024-eb612d936fde h1:eMHY9dMDkNuDMWhfTbMZHbbsxj7G6mfujjKei1HaFQM= go.mau.fi/util v0.9.6 h1:2nsvxm49KhI3wrFltr0+wSUBlnQ4CMtykuELjpIU+ts=
go.mau.fi/util v0.10.1-0.20260820140024-eb612d936fde/go.mod h1:z0ZZNt4hq3FZbUKnunexE/QscCx7VkLvQSvtggc/aE8= go.mau.fi/util v0.9.6/go.mod h1:sIJpRH7Iy5Ad1SBuxQoatxtIeErgzxCtjd/2hCMkYMI=
go.mau.fi/webp v0.3.0 h1:gVHQZtz21Ziwj+CDuklbX9mqpsnDIFKxs/BJyV7iZzA= go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.3.0/go.mod h1:rlZFTev+dYxhvk+XNBP/5GcTt4gXmzAB4DU0aGUYIQo= go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
go.mau.fi/whatsmeow v0.0.0-20260821141805-33cfac511629 h1:99p9fTS1G73a1aifCVaV5MX4hDq0lVUKWGoUNoXyE3A= go.mau.fi/whatsmeow v0.0.0-20260218131543-e4d82a04d5d8 h1:riEnRpKjNnVLuaGIm+9Q3SozatMsseJLxnc3ZWH6Exo=
go.mau.fi/whatsmeow v0.0.0-20260821141805-33cfac511629/go.mod h1:aMd13H2xFFGH9cskcvxo4Aae+TmyFN38yw+HvsrpwVg= 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 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w= go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 h1:YXnL44eJ77R+ji4/ooy8UsXIhz+lbi2Qgdlc8iRN0gY= golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297/go.mod h1:Mkmymgv+uMpSQ/XxJ/7GpdrdYoqm3u72jEbpCLiJmNk= golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0= golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4= golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To= golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU= golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek= golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= 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.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= 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 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@ -107,5 +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= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
maunium.net/go/mautrix v0.30.1-0.20260820142923-ae00f2c62e1e h1:ATvERyBIUXJrnL7ezP2xCLLGCNmGClmfzoQq4N3jBcA= maunium.net/go/mautrix v0.26.3 h1:tWZih6Vjw0qGTWuPmg9JUrQPzViTNDPGQLVc5UXC4nk=
maunium.net/go/mautrix v0.30.1-0.20260820142923-ae00f2c62e1e/go.mod h1:Y02sBiAvfEVqK24bwVGCprmLATRZ7prWel3ZpB413e0= maunium.net/go/mautrix v0.26.3/go.mod h1:v5ZdDoCwUpNqEj5OrhEoUa3L1kEddKPaAya9TgGXN38=

View file

@ -12,7 +12,6 @@ import (
"time" "time"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.mau.fi/util/exmaps"
"go.mau.fi/util/ptr" "go.mau.fi/util/ptr"
"go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/proto/waE2E" "go.mau.fi/whatsmeow/proto/waE2E"
@ -64,6 +63,11 @@ func (wa *WhatsAppClient) historySyncLoop(ctx context.Context) {
for { for {
var resetTimer bool var resetTimer bool
select { 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: case <-wa.historySyncWakeup:
dispatchTimer.Stop() dispatchTimer.Stop()
notif, rowid, err := wa.Main.DB.HSNotif.GetNext(ctx, wa.UserLogin.ID) 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("chunk_order", evt.GetChunkOrder()).
Uint32("progress", evt.GetProgress()). Uint32("progress", evt.GetProgress()).
Logger() Logger()
log.Debug(). log.Debug().Msg("Downloading history sync")
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")
blob, err := wa.Client.DownloadHistorySync(log.WithContext(ctx), evt, true) blob, err := wa.Client.DownloadHistorySync(log.WithContext(ctx), evt, true)
if err != nil { if err != nil {
log.Err(err).Msg("Failed to download history sync") log.Err(err).Msg("Failed to download history sync")
return 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) { 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 { if innerErr != nil {
return return
} }
@ -151,28 +139,13 @@ func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context,
}) })
if err != nil { if err != nil {
log.Err(err).Msg("Failed to store history sync notification data") 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 return
} }
func (wa *WhatsAppClient) handleWAHistorySync( func (wa *WhatsAppClient) handleWAHistorySync(ctx context.Context, evt *waHistorySync.HistorySync, stopOnError bool) (bool, error) {
ctx context.Context,
notif *waE2E.HistorySyncNotification,
evt *waHistorySync.HistorySync,
stopOnError bool,
) error {
if evt == nil || evt.SyncType == nil { if evt == nil || evt.SyncType == nil {
return nil return false, nil
} }
log := wa.UserLogin.Log.With(). log := wa.UserLogin.Log.With().
Str("action", "store history sync"). Str("action", "store history sync").
@ -197,16 +170,11 @@ func (wa *WhatsAppClient) handleWAHistorySync(
Int("recent_sticker_count", len(evt.GetRecentStickers())). Int("recent_sticker_count", len(evt.GetRecentStickers())).
Int("past_participant_count", len(evt.GetPastParticipants())). Int("past_participant_count", len(evt.GetPastParticipants())).
Msg("Ignoring history sync") Msg("Ignoring history sync")
return nil return false, nil
} }
log.Info(). log.Info().
Int("conversation_count", len(evt.GetConversations())). Int("conversation_count", len(evt.GetConversations())).
Int("past_participant_count", len(evt.GetPastParticipants())). 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") Msg("Storing history sync")
start := time.Now() start := time.Now()
successfullySavedTotal := 0 successfullySavedTotal := 0
@ -229,32 +197,30 @@ func (wa *WhatsAppClient) handleWAHistorySync(
} else { } else {
totalMessageCount += len(conv.GetMessages()) totalMessageCount += len(conv.GetMessages())
} }
if jid.Server == types.DefaultUserServer { if jid.Server == types.HiddenUserServer {
lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, jid) pn, err := wa.GetStore().LIDs.GetPNForLID(ctx, jid)
if err != nil { if err != nil {
log.Err(err).Stringer("pn", jid).Msg("Failed to get LID for phone number in history sync") log.Err(err).Stringer("lid", jid).Msg("Failed to get PN for LID in history sync")
} else if lid.IsEmpty() { } else if pn.IsEmpty() {
log.Warn().Stringer("pn", jid).Msg("No LID found for phone number in history sync") log.Warn().Stringer("lid", jid).Msg("No PN found for LID in history sync")
} else { } else {
log.Debug(). log.Debug().
Stringer("lid", lid). Stringer("lid", jid).
Stringer("pn", jid). Stringer("pn", pn).
Msg("Rerouting phone number DM to LID in history sync") Msg("Rerouting LID DM to phone number in history sync")
jid = lid jid = pn
} }
} }
log.UpdateContext(func(c zerolog.Context) zerolog.Context { log.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.Stringer("chat_jid", jid) return c.Stringer("chat_jid", jid)
}) })
var firstItemTime, lastItemTime time.Time var minTime, maxTime time.Time
var minTimeIndex, maxTimeIndex int
ignoredTypes := 0 ignoredTypes := 0
rawMessages := conv.GetMessages() messages := make([]*wadb.HistorySyncMessageTuple, 0, len(conv.GetMessages()))
messages := make([]*wadb.HistorySyncMessageTuple, 0, len(rawMessages)) for i, rawMsg := range conv.GetMessages() {
allowClamp := conv.GetCommentsCount() == 0
var newerTS uint64
for i, rawMsg := range rawMessages {
// Don't store messages that will just be skipped. // Don't store messages that will just be skipped.
msgEvt, err := wa.Client.ParseWebMessage(jid, rawMsg.GetMessage()) msgEvt, err := wa.Client.ParseWebMessage(jid, rawMsg.GetMessage())
if err != nil { if err != nil {
@ -263,39 +229,22 @@ func (wa *WhatsAppClient) handleWAHistorySync(
Str("msg_id", rawMsg.GetMessage().GetKey().GetID()). Str("msg_id", rawMsg.GetMessage().GetKey().GetID()).
Uint64("msg_time_seconds", rawMsg.GetMessage().GetMessageTimestamp()). Uint64("msg_time_seconds", rawMsg.GetMessage().GetMessageTimestamp()).
Msg("Dropping historical message due to parse error") 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 continue
} }
if firstItemTime.IsZero() { if minTime.IsZero() || msgEvt.Info.Timestamp.Before(minTime) {
firstItemTime = msgEvt.Info.Timestamp 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) msgType := getMessageType(msgEvt.Message)
if msgType == "ignore" || strings.HasPrefix(msgType, "unknown_protocol_") { if msgType == "ignore" || strings.HasPrefix(msgType, "unknown_protocol_") {
ignoredTypes++ ignoredTypes++
continue continue
} }
// Comments (replies) in announcement groups are not ordered by timestamp, so don't clamp them.
if rawMsg.GetMessage().GetCommentMetadata().GetCommentParentKey() != nil {
allowClamp = false
}
// WhatsApp has bugs where some random messages will have timestamps decades in the future.
// To ensure they don't mess up our ordering, require timestamps of older messages to be
// before the previous (newer) message.
if currentTS := rawMsg.GetMessage().GetMessageTimestamp(); newerTS > 0 && allowClamp && currentTS > newerTS {
log.Warn().
Time("current_ts", time.Unix(int64(currentTS), 0)).
Time("prev_ts", time.Unix(int64(newerTS), 0)).
Int("msg_index", i).
Str("msg_id", rawMsg.GetMessage().GetKey().GetID()).
Msg("Clamping message timestamp")
rawMsg.Message.MessageTimestamp = ptr.Ptr(newerTS)
msgEvt.Info.Timestamp = time.Unix(int64(newerTS), 0)
}
newerTS = rawMsg.GetMessage().GetMessageTimestamp()
marshaled, err := proto.Marshal(rawMsg) marshaled, err := proto.Marshal(rawMsg)
if err != nil { if err != nil {
log.Warn().Err(err). log.Warn().Err(err).
@ -310,8 +259,10 @@ func (wa *WhatsAppClient) handleWAHistorySync(
log.Debug(). log.Debug().
Int("wrapped_count", len(messages)). Int("wrapped_count", len(messages)).
Int("ignored_msg_type_count", ignoredTypes). Int("ignored_msg_type_count", ignoredTypes).
Time("first_item_time", firstItemTime). Time("lowest_time", minTime).
Time("last_item_time", lastItemTime). Int("lowest_time_index", minTimeIndex).
Time("highest_time", maxTime).
Int("highest_time_index", maxTimeIndex).
Dict("metadata", zerolog.Dict(). Dict("metadata", zerolog.Dict().
Uint32("ephemeral_expiration", conv.GetEphemeralExpiration()). Uint32("ephemeral_expiration", conv.GetEphemeralExpiration()).
Int64("ephemeral_setting_timestamp", conv.GetEphemeralSettingTimestamp()). Int64("ephemeral_setting_timestamp", conv.GetEphemeralSettingTimestamp()).
@ -320,17 +271,15 @@ func (wa *WhatsAppClient) handleWAHistorySync(
Bool("archived", conv.GetArchived()). Bool("archived", conv.GetArchived()).
Uint32("pinned", conv.GetPinned()). Uint32("pinned", conv.GetPinned()).
Uint64("mute_end", conv.GetMuteEndTime()). Uint64("mute_end", conv.GetMuteEndTime()).
Uint32("unread_count", conv.GetUnreadCount()). Uint32("unread_count", conv.GetUnreadCount()),
Bool("end_of_history", conv.GetEndOfHistoryTransfer()).
Stringer("end_of_history_type", conv.GetEndOfHistoryTransferType()),
). ).
Msg("Collected messages to save from history sync conversation") Msg("Collected messages to save from history sync conversation")
if len(messages) > 0 { if len(messages) > 0 {
err = wa.Main.DB.Conversation.Put(ctx, wadb.NewConversation(wa.UserLogin.ID, jid, conv, firstItemTime)) err = wa.Main.DB.Conversation.Put(ctx, wadb.NewConversation(wa.UserLogin.ID, jid, conv, maxTime))
if err != nil { if err != nil {
if stopOnError { if stopOnError {
return fmt.Errorf("failed to save conversation metadata for %s: %w", jid, err) return false, fmt.Errorf("failed to save conversation metadata for %s: %w", jid, err)
} }
log.Err(err).Msg("Failed to save conversation metadata") log.Err(err).Msg("Failed to save conversation metadata")
continue continue
@ -338,7 +287,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
err = wa.Main.DB.Message.Put(ctx, wa.UserLogin.ID, jid, messages) err = wa.Main.DB.Message.Put(ctx, wa.UserLogin.ID, jid, messages)
if err != nil { if err != nil {
if stopOnError { 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") log.Err(err).Msg("Failed to save messages")
failedToSaveTotal += len(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) err = wa.Main.Bridge.DB.BackfillTask.MarkNotDone(ctx, wa.makeWAPortalKey(jid), wa.UserLogin.ID)
if err != nil { if err != nil {
if stopOnError { 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") 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). Int("total_message_count", totalMessageCount).
Dur("duration", time.Since(start)). Dur("duration", time.Since(start)).
Msg("Finished storing history sync") 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) { func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) {
@ -408,8 +359,8 @@ func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) {
return return
} }
wrappedInfo, err := wa.getChatInfo(ctx, conv.ChatJID, conv, true) wrappedInfo, err := wa.getChatInfo(ctx, conv.ChatJID, conv, true)
if errors.Is(err, whatsmeow.ErrNotInGroup) || errors.Is(err, whatsmeow.ErrGroupNotFound) { if errors.Is(err, whatsmeow.ErrNotInGroup) {
log.Debug().Err(err).Stringer("chat_jid", conv.ChatJID). log.Debug().Stringer("chat_jid", conv.ChatJID).
Msg("Skipping creating room because the user is not a participant") Msg("Skipping creating room because the user is not a participant")
//err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, conv.ChatJID) //err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, conv.ChatJID)
//if err != nil { //if err != nil {
@ -482,72 +433,40 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
if err != nil { if err != nil {
return nil, err return nil, err
} }
if portalJID.Server == types.DefaultUserServer {
zerolog.Ctx(ctx).Warn().Stringer("portal_jid", portalJID).Msg("FetchMessages called for phone number portal")
}
var markRead bool var markRead bool
var startTime, endTime *time.Time var startTime, endTime *time.Time
var conv *wadb.Conversation
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.Forward {
if params.AnchorMessage != nil { if params.AnchorMessage != nil {
startTime = ptr.Ptr(params.AnchorMessage.Timestamp) 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 markRead = !ptr.Val(conv.MarkedAsUnread) && ptr.Val(conv.UnreadCount) == 0
} }
} else { } else if params.Cursor != "" {
if params.AnchorMessage != nil { endTimeUnix, err := strconv.ParseInt(string(params.Cursor), 10, 64)
endTime = ptr.Ptr(params.AnchorMessage.Timestamp) if err != nil {
return nil, fmt.Errorf("failed to parse cursor: %w", err)
} }
if params.Cursor != "" { endTime = ptr.Ptr(time.Unix(endTimeUnix, 0))
endTimeUnix, err := strconv.ParseInt(string(params.Cursor), 10, 64) } else if params.AnchorMessage != nil {
if err != nil { endTime = ptr.Ptr(params.AnchorMessage.Timestamp)
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
} }
messages, err := wa.Main.DB.Message.GetBetween(ctx, wa.UserLogin.ID, portalJID, startTime, endTime, params.Count+1) messages, err := wa.Main.DB.Message.GetBetween(ctx, wa.UserLogin.ID, portalJID, startTime, endTime, params.Count+1)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to load messages from database: %w", err) 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) { } else if len(messages) == 0 {
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)
}
return &bridgev2.FetchMessagesResponse{ return &bridgev2.FetchMessagesResponse{
HasMore: false, HasMore: false,
Forward: params.Forward, Forward: params.Forward,
}, nil }, nil
} }
hasMore := false
oldestTS := messages[len(messages)-1].GetMessageTimestamp()
newestTS := messages[0].GetMessageTimestamp()
if len(messages) > params.Count { if len(messages) > params.Count {
oldestTS := messages[len(messages)-1].GetMessageTimestamp()
hasMore = true hasMore = true
// For safety, cut off messages with the oldest timestamp in the response. // 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. // 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) convertedMessages := make([]*bridgev2.BackfillMessage, len(messages))
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))
var mediaRequests []*wadb.MediaRequest var mediaRequests []*wadb.MediaRequest
dups := make(exmaps.Set[networkid.MessageID])
for i, msg := range messages { for i, msg := range messages {
evt, err := wa.Client.ParseWebMessage(portalJID, msg) evt, err := wa.Client.ParseWebMessage(portalJID, msg)
if err != nil { if err != nil {
if explodeOnError { // This should never happen because the info is already parsed once before being stored in the database
// 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)
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)
} }
var mediaReq *wadb.MediaRequest
isViewOnce := evt.IsViewOnce || evt.IsViewOnceV2 || evt.IsViewOnceV2Extension isViewOnce := evt.IsViewOnce || evt.IsViewOnceV2 || evt.IsViewOnceV2Extension
converted, mediaReq := wa.convertHistorySyncMessage( convertedMessages[i], mediaReq = wa.convertHistorySyncMessage(
ctx, portal, &evt.Info, evt.Message, evt.RawMessage, isViewOnce, msg.Reactions, 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 { if mediaReq != nil {
mediaRequests = append(mediaRequests, mediaReq) mediaRequests = append(mediaRequests, mediaReq)
} }
@ -650,10 +498,24 @@ func (wa *WhatsAppClient) convertHistorySyncMessages(
return &bridgev2.FetchMessagesResponse{ return &bridgev2.FetchMessagesResponse{
Messages: convertedMessages, Messages: convertedMessages,
Cursor: networkid.PaginationCursor(strconv.FormatUint(oldestTS, 10)), Cursor: networkid.PaginationCursor(strconv.FormatUint(oldestTS, 10)),
HasMore: hasMore,
Forward: endTime == nil,
MarkRead: markRead,
// TODO set remaining or total count
CompleteCallback: func() { CompleteCallback: func() {
// TODO this only deletes after backfilling. If there's no need for backfill after a relogin, // TODO this only deletes after backfilling. If there's no need for backfill after a relogin,
// the messages will be stuck in the database // 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 { if len(mediaRequests) > 0 {
go func(ctx context.Context) { go func(ctx context.Context) {
for _, req := range mediaRequests { for _, req := range mediaRequests {
@ -671,109 +533,6 @@ func (wa *WhatsAppClient) convertHistorySyncMessages(
}, nil }, 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( func (wa *WhatsAppClient) convertHistorySyncMessage(
ctx context.Context, portal *bridgev2.Portal, info *types.MessageInfo, msg, rawMsg *waE2E.Message, isViewOnce bool, reactions []*waWeb.Reaction, ctx context.Context, portal *bridgev2.Portal, info *types.MessageInfo, msg, rawMsg *waE2E.Message, isViewOnce bool, reactions []*waWeb.Reaction,
) (*bridgev2.BackfillMessage, *wadb.MediaRequest) { ) (*bridgev2.BackfillMessage, *wadb.MediaRequest) {
@ -784,12 +543,11 @@ func (wa *WhatsAppClient) convertHistorySyncMessage(
} }
// TODO use proper intent // TODO use proper intent
intent := wa.Main.Bridge.Bot intent := wa.Main.Bridge.Bot
msgID := waid.MakeMessageIDWithAltSender(info.Chat, info.Sender, info.SenderAlt, info.ID)
wrapped := &bridgev2.BackfillMessage{ wrapped := &bridgev2.BackfillMessage{
ConvertedMessage: wa.Main.MsgConv.ToMatrix(ctx, portal, wa.Client, intent, msg, rawMsg, info, isViewOnce, true, nil), ConvertedMessage: wa.Main.MsgConv.ToMatrix(ctx, portal, wa.Client, intent, msg, rawMsg, info, isViewOnce, true, nil),
Sender: wa.makeEventSender(ctx, pickLID(info.Sender, info.SenderAlt)), Sender: wa.makeEventSender(ctx, info.Sender),
ID: msgID, ID: waid.MakeMessageID(info.Chat, info.Sender, info.ID),
TxnID: networkid.TransactionID(msgID), TxnID: networkid.TransactionID(waid.MakeMessageID(info.Chat, info.Sender, info.ID)),
Timestamp: info.Timestamp, Timestamp: info.Timestamp,
StreamOrder: info.Timestamp.Unix(), StreamOrder: info.Timestamp.Unix(),
Reactions: make([]*bridgev2.BackfillReaction, 0, len(reactions)), Reactions: make([]*bridgev2.BackfillReaction, 0, len(reactions)),
@ -798,10 +556,10 @@ func (wa *WhatsAppClient) convertHistorySyncMessage(
for _, reaction := range reactions { for _, reaction := range reactions {
var sender types.JID var sender types.JID
if reaction.GetKey().GetFromMe() { if reaction.GetKey().GetFromMe() {
sender = wa.GetLID() sender = wa.JID
} else if reaction.GetKey().GetParticipant() != "" { } else if reaction.GetKey().GetParticipant() != "" {
sender, _ = types.ParseJID(*reaction.Key.Participant) sender, _ = types.ParseJID(*reaction.Key.Participant)
} else if info.Chat.Server == types.DefaultUserServer || info.Chat.Server == types.HiddenUserServer || info.Chat.Server == types.BotServer { } else if info.Chat.Server == types.DefaultUserServer || info.Chat.Server == types.BotServer {
sender = info.Chat sender = info.Chat
} }
if sender.IsEmpty() { if sender.IsEmpty() {

View file

@ -19,7 +19,6 @@ var WhatsAppGeneralCaps = &bridgev2.NetworkGeneralCapabilities{
AggressiveUpdateInfo: true, AggressiveUpdateInfo: true,
ImplicitReadReceipts: true, ImplicitReadReceipts: true,
Provisioning: bridgev2.ProvisioningCapabilities{ Provisioning: bridgev2.ProvisioningCapabilities{
ImagePackImport: true,
ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{ ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{
CreateDM: true, CreateDM: true,
LookupPhone: true, LookupPhone: true,
@ -52,7 +51,7 @@ func (wa *WhatsAppConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilit
} }
func (wa *WhatsAppConnector) GetBridgeInfoVersion() (info, caps int) { func (wa *WhatsAppConnector) GetBridgeInfoVersion() (info, caps int) {
return 1, 9 return 1, 7
} }
const WAMaxFileSize = 2000 * 1024 * 1024 const WAMaxFileSize = 2000 * 1024 * 1024
@ -67,7 +66,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel {
} }
func capID() string { func capID() string {
base := "fi.mau.whatsapp.capabilities.2026_07_22" base := "fi.mau.whatsapp.capabilities.2025_12_15"
if ffmpeg.Supported() { if ffmpeg.Supported() {
return base + "+ffmpeg" return base + "+ffmpeg"
} }
@ -126,10 +125,10 @@ var whatsappCaps = &event.RoomFeatures{
event.CapMsgSticker: { event.CapMsgSticker: {
MimeTypes: map[string]event.CapabilitySupportLevel{ MimeTypes: map[string]event.CapabilitySupportLevel{
"image/webp": event.CapLevelFullySupported, "image/webp": event.CapLevelFullySupported,
// TODO see if sending lottie is possible
//"video/lottie+json": event.CapLevelFullySupported,
"image/png": event.CapLevelPartialSupport, "image/png": event.CapLevelPartialSupport,
"image/jpeg": 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, Caption: event.CapLevelDropped,
MaxSize: WAMaxFileSize, MaxSize: WAMaxFileSize,
@ -174,26 +173,21 @@ var whatsappCaps = &event.RoomFeatures{
event.MemberActionKick: event.CapLevelFullySupported, event.MemberActionKick: event.CapLevelFullySupported,
event.MemberActionLeave: event.CapLevelFullySupported, event.MemberActionLeave: event.CapLevelFullySupported,
}, },
MaxTextLength: MaxTextLength, MaxTextLength: MaxTextLength,
LocationMessage: event.CapLevelFullySupported, LocationMessage: event.CapLevelFullySupported,
Poll: event.CapLevelFullySupported, Poll: event.CapLevelFullySupported,
PollEnd: event.CapLevelUnsupported, Reply: event.CapLevelFullySupported,
PollHiddenVotes: event.CapLevelUnsupported, Edit: event.CapLevelFullySupported,
PollDuplicateOptions: event.CapLevelUnsupported, EditMaxAge: ptr.Ptr(jsontime.S(EditMaxAge)),
PollMaxOptions: 12, Delete: event.CapLevelFullySupported,
PollOptionMaxLength: 100, DeleteForMe: false,
Reply: event.CapLevelFullySupported, DeleteMaxAge: ptr.Ptr(jsontime.S(2 * 24 * time.Hour)),
Edit: event.CapLevelFullySupported, Reaction: event.CapLevelFullySupported,
EditMaxAge: ptr.Ptr(jsontime.S(EditMaxAge)), ReactionCount: 1,
Delete: event.CapLevelFullySupported, ReadReceipts: true,
DeleteForMe: false, TypingNotifications: true,
DeleteMaxAge: ptr.Ptr(jsontime.S(2 * 24 * time.Hour)), DisappearingTimer: waDisappearingCap,
Reaction: event.CapLevelFullySupported, DeleteChat: true,
ReactionCount: 1,
ReadReceipts: true,
TypingNotifications: true,
DisappearingTimer: waDisappearingCap,
DeleteChat: true,
} }
var whatsappDMCaps *event.RoomFeatures var whatsappDMCaps *event.RoomFeatures

View file

@ -99,18 +99,6 @@ func (wa *WhatsAppClient) applyChatSettings(ctx context.Context, chatID types.JI
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get chat settings") zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get chat settings")
return return
} }
if !chat.Found {
chatID, err = wa.GetStore().GetAltJID(ctx, chatID)
if err != nil {
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get alternate JID to get chat settings")
return
}
chat, err = wa.GetStore().ChatSettings.GetChatSettings(ctx, chatID)
if err != nil {
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get chat settings with alternate JID")
return
}
}
info.UserLocal = &bridgev2.UserLocalPortalInfo{ info.UserLocal = &bridgev2.UserLocalPortalInfo{
MutedUntil: ptr.Ptr(chat.MutedUntil), MutedUntil: ptr.Ptr(chat.MutedUntil),
} }
@ -153,20 +141,15 @@ const PrivateChatTopic = "WhatsApp private chat"
const BotChatTopic = "WhatsApp chat with a bot" const BotChatTopic = "WhatsApp chat with a bot"
func (wa *WhatsAppClient) wrapDMInfo(ctx context.Context, jid types.JID) *bridgev2.ChatInfo { func (wa *WhatsAppClient) wrapDMInfo(ctx context.Context, jid types.JID) *bridgev2.ChatInfo {
ownID := wa.JID
if jid.Server == types.HiddenUserServer {
ownID = wa.GetLID()
}
info := &bridgev2.ChatInfo{ info := &bridgev2.ChatInfo{
Type: ptr.Ptr(database.RoomTypeDM),
Topic: ptr.Ptr(PrivateChatTopic), Topic: ptr.Ptr(PrivateChatTopic),
Members: &bridgev2.ChatMemberList{ Members: &bridgev2.ChatMemberList{
IsFull: true, IsFull: true,
TotalMemberCount: 2, TotalMemberCount: 2,
OtherUserID: waid.MakeUserID(jid), OtherUserID: waid.MakeUserID(jid),
MemberMap: map[networkid.UserID]bridgev2.ChatMember{ MemberMap: map[networkid.UserID]bridgev2.ChatMember{
waid.MakeUserID(jid): {EventSender: wa.makeEventSender(ctx, jid)}, waid.MakeUserID(jid): {EventSender: wa.makeEventSender(ctx, jid)},
waid.MakeUserID(ownID): {EventSender: wa.makeEventSender(ctx, ownID)}, waid.MakeUserID(wa.JID): {EventSender: wa.makeEventSender(ctx, wa.JID)},
}, },
PowerLevels: &bridgev2.PowerLevelOverrides{ PowerLevels: &bridgev2.PowerLevelOverrides{
Events: map[event.Type]int{ Events: map[event.Type]int{
@ -176,14 +159,13 @@ func (wa *WhatsAppClient) wrapDMInfo(ctx context.Context, jid types.JID) *bridge
event.StateBeeperDisappearingTimer: 0, event.StateBeeperDisappearingTimer: 0,
}, },
}, },
ExcludeChangesFromTimeline: true,
}, },
ExcludeChangesFromTimeline: true, Type: ptr.Ptr(database.RoomTypeDM),
} }
if jid.Server == types.BotServer { if jid.Server == types.BotServer {
info.Topic = ptr.Ptr(BotChatTopic) info.Topic = ptr.Ptr(BotChatTopic)
} }
if wa.IsOwnJID(jid) { if jid == wa.JID.ToNonAD() {
// For chats with self, force-split the members so the user's own ghost is always in the room. // For chats with self, force-split the members so the user's own ghost is always in the room.
info.Members.MemberMap = map[networkid.UserID]bridgev2.ChatMember{ info.Members.MemberMap = map[networkid.UserID]bridgev2.ChatMember{
waid.MakeUserID(jid): {EventSender: bridgev2.EventSender{Sender: waid.MakeUserID(jid)}}, waid.MakeUserID(jid): {EventSender: bridgev2.EventSender{Sender: waid.MakeUserID(jid)}},
@ -207,7 +189,7 @@ func (wa *WhatsAppClient) wrapStatusBroadcastInfo(ctx context.Context) *bridgev2
Members: &bridgev2.ChatMemberList{ Members: &bridgev2.ChatMemberList{
IsFull: false, IsFull: false,
MemberMap: map[networkid.UserID]bridgev2.ChatMember{ MemberMap: map[networkid.UserID]bridgev2.ChatMember{
waid.MakeUserID(wa.GetLID()): {EventSender: wa.makeEventSender(ctx, wa.GetLID())}, waid.MakeUserID(wa.JID): {EventSender: wa.makeEventSender(ctx, wa.JID)},
}, },
}, },
Type: ptr.Ptr(database.RoomTypeDefault), Type: ptr.Ptr(database.RoomTypeDefault),
@ -275,7 +257,7 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
Name: ptr.Ptr(info.Name), Name: ptr.Ptr(info.Name),
Topic: ptr.Ptr(info.Topic), Topic: ptr.Ptr(info.Topic),
Members: &bridgev2.ChatMemberList{ Members: &bridgev2.ChatMemberList{
IsFull: !info.IsIncognito && !info.IsParent, IsFull: !info.IsIncognito,
TotalMemberCount: len(info.Participants), TotalMemberCount: len(info.Participants),
MemberMap: make(map[networkid.UserID]bridgev2.ChatMember, len(info.Participants)), MemberMap: make(map[networkid.UserID]bridgev2.ChatMember, len(info.Participants)),
PowerLevels: &bridgev2.PowerLevelOverrides{ PowerLevels: &bridgev2.PowerLevelOverrides{
@ -284,13 +266,14 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
Ban: ptr.Ptr(nobodyPL), Ban: ptr.Ptr(nobodyPL),
// TODO allow invites if bridge config says to allow them, or maybe if relay mode is enabled? // TODO allow invites if bridge config says to allow them, or maybe if relay mode is enabled?
Events: map[event.Type]int{ Events: map[event.Type]int{
event.StateRoomName: metaChangePL, event.StateRoomName: metaChangePL,
event.StateRoomAvatar: metaChangePL, event.StateRoomAvatar: metaChangePL,
event.StateTopic: metaChangePL, event.StateTopic: metaChangePL,
event.EventReaction: defaultPL, event.EventReaction: defaultPL,
event.EventRedaction: defaultPL, event.EventRedaction: defaultPL,
event.EventUnstablePollResponse: defaultPL,
event.StateBeeperDisappearingTimer: metaChangePL, event.StateBeeperDisappearingTimer: metaChangePL,
// TODO always allow poll responses
}, },
}, },
}, },
@ -301,15 +284,11 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
}, },
ExtraUpdates: extraUpdater, ExtraUpdates: extraUpdater,
} }
var hasSelf bool
for _, pcp := range info.Participants { for _, pcp := range info.Participants {
member := bridgev2.ChatMember{ member := bridgev2.ChatMember{
EventSender: wa.makeEventSender(ctx, pcp.JID), EventSender: wa.makeEventSender(ctx, pcp.JID),
Membership: event.MembershipJoin, Membership: event.MembershipJoin,
} }
if member.EventSender.IsFromMe {
hasSelf = true
}
if pcp.IsSuperAdmin { if pcp.IsSuperAdmin {
member.PowerLevel = ptr.Ptr(superAdminPL) member.PowerLevel = ptr.Ptr(superAdminPL)
} else if pcp.IsAdmin { } else if pcp.IsAdmin {
@ -320,21 +299,18 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
member.MemberEventExtra = map[string]any{ member.MemberEventExtra = map[string]any{
"com.beeper.exclude_from_timeline": true, "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() { 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)}, EventSender: bridgev2.EventSender{Sender: waid.MakeUserID(pcp.PhoneNumber)},
Membership: event.MembershipLeave, Membership: event.MembershipLeave,
PrevMembership: event.MembershipJoin, PrevMembership: event.MembershipJoin,
MemberEventExtra: map[string]any{ MemberEventExtra: map[string]any{
"com.beeper.exclude_from_timeline": true, "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() { if !info.LinkedParentJID.IsEmpty() {
wrapped.ParentID = ptr.Ptr(waid.MakePortalID(info.LinkedParentJID)) wrapped.ParentID = ptr.Ptr(waid.MakePortalID(info.LinkedParentJID))
@ -474,7 +450,7 @@ func (wa *WhatsAppClient) makePortalAvatarFetcher(avatarID string, sender types.
wrappedAvatar = &bridgev2.Avatar{ wrappedAvatar = &bridgev2.Avatar{
ID: networkid.AvatarID(avatar.ID), ID: networkid.AvatarID(avatar.ID),
Get: func(ctx context.Context) ([]byte, error) { 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 { if info.ThreadMeta.Picture != nil {
avatar.ID = networkid.AvatarID(info.ThreadMeta.Picture.ID) avatar.ID = networkid.AvatarID(info.ThreadMeta.Picture.ID)
avatar.Get = func(ctx context.Context) ([]byte, error) { 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 != "" { } else if info.ThreadMeta.Preview.ID != "" {
avatar.ID = networkid.AvatarID(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 { } else if meta.ThreadMeta.Picture == nil {
return nil, fmt.Errorf("full res avatar info is missing") 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 { } else {
avatar.ID = "remove" avatar.ID = "remove"
@ -540,8 +516,8 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne
Members: &bridgev2.ChatMemberList{ Members: &bridgev2.ChatMemberList{
TotalMemberCount: info.ThreadMeta.SubscriberCount, TotalMemberCount: info.ThreadMeta.SubscriberCount,
MemberMap: map[networkid.UserID]bridgev2.ChatMember{ MemberMap: map[networkid.UserID]bridgev2.ChatMember{
waid.MakeUserID(wa.GetLID()): { waid.MakeUserID(wa.JID): {
EventSender: wa.makeEventSender(ctx, wa.GetLID()), EventSender: wa.makeEventSender(ctx, wa.JID),
PowerLevel: &ownPowerLevel, PowerLevel: &ownPowerLevel,
}, },
}, },
@ -550,12 +526,12 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne
StateDefault: ptr.Ptr(nobodyPL), StateDefault: ptr.Ptr(nobodyPL),
Ban: ptr.Ptr(nobodyPL), Ban: ptr.Ptr(nobodyPL),
Events: map[event.Type]int{ Events: map[event.Type]int{
event.StateRoomName: adminPL, event.StateRoomName: adminPL,
event.StateRoomAvatar: adminPL, event.StateRoomAvatar: adminPL,
event.StateTopic: adminPL, event.StateTopic: adminPL,
event.EventReaction: defaultPL, event.EventReaction: defaultPL,
event.EventRedaction: defaultPL, event.EventRedaction: defaultPL,
event.EventUnstablePollResponse: defaultPL, // TODO always allow poll responses
}, },
}, },
}, },

View file

@ -29,6 +29,7 @@ import (
"go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/appstate" "go.mau.fi/whatsmeow/appstate"
waBinary "go.mau.fi/whatsmeow/binary" waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/proto/waHistorySync"
"go.mau.fi/whatsmeow/proto/waWa6" "go.mau.fi/whatsmeow/proto/waWa6"
"go.mau.fi/whatsmeow/store" "go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types"
@ -38,7 +39,6 @@ import (
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/bridgev2/status" "maunium.net/go/mautrix/bridgev2/status"
"maunium.net/go/mautrix/event"
"go.mau.fi/mautrix-whatsapp/pkg/waid" "go.mau.fi/mautrix-whatsapp/pkg/waid"
) )
@ -49,14 +49,14 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2.
UserLogin: login, UserLogin: login,
MC: noopMCInstance, MC: noopMCInstance,
historySyncs: make(chan *waHistorySync.HistorySync, 64),
historySyncWakeup: make(chan struct{}, 1), historySyncWakeup: make(chan struct{}, 1),
resyncQueue: make(map[types.JID]resyncQueueItem), resyncQueue: make(map[types.JID]resyncQueueItem),
directMediaRetries: make(map[networkid.MessageID]*directMediaRetry), directMediaRetries: make(map[networkid.MessageID]*directMediaRetry),
mediaRetryLock: semaphore.NewWeighted(wa.Config.HistorySync.MediaRequests.MaxAsyncHandle), mediaRetryLock: semaphore.NewWeighted(wa.Config.HistorySync.MediaRequests.MaxAsyncHandle),
pushNamesSynced: exsync.NewEvent(), pushNamesSynced: exsync.NewEvent(),
createDedup: exsync.NewSet[types.MessageID](),
appStateFullSyncAttempted: make(map[appstate.WAPatchName]time.Time), appStateFullSyncAttempted: make(map[appstate.WAPatchName]time.Time),
disableNewsletter: store.BaseClientPayload.GetUserAgent().GetPlatform() == waWa6.ClientPayload_UserAgent_MACOS,
} }
login.Client = w login.Client = w
@ -71,7 +71,6 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2.
if err != nil { if err != nil {
return err return err
} }
w.LID = w.Device.GetLID()
if w.Device != nil { if w.Device != nil {
log := w.UserLogin.Log.With().Str("component", "whatsmeow").Logger() log := w.UserLogin.Log.With().Str("component", "whatsmeow").Logger()
@ -106,9 +105,9 @@ type WhatsAppClient struct {
Client *whatsmeow.Client Client *whatsmeow.Client
Device *store.Device Device *store.Device
JID types.JID JID types.JID
LID types.JID
MC mClient MC mClient
historySyncs chan *waHistorySync.HistorySync
historySyncWakeup chan struct{} historySyncWakeup chan struct{}
stopLoops atomic.Pointer[context.CancelFunc] stopLoops atomic.Pointer[context.CancelFunc]
resyncQueue map[types.JID]resyncQueueItem resyncQueue map[types.JID]resyncQueueItem
@ -121,8 +120,7 @@ type WhatsAppClient struct {
isNewLogin bool isNewLogin bool
pushNamesSynced *exsync.Event pushNamesSynced *exsync.Event
lastPresence types.Presence lastPresence types.Presence
createDedup *exsync.Set[types.MessageID]
disableNewsletter bool
appStateRecoveryLock sync.Mutex appStateRecoveryLock sync.Mutex
appStateFullSyncAttempted map[appstate.WAPatchName]time.Time appStateFullSyncAttempted map[appstate.WAPatchName]time.Time
@ -133,7 +131,6 @@ var (
_ bridgev2.PushableNetworkAPI = (*WhatsAppClient)(nil) _ bridgev2.PushableNetworkAPI = (*WhatsAppClient)(nil)
_ bridgev2.BackgroundSyncingNetworkAPI = (*WhatsAppClient)(nil) _ bridgev2.BackgroundSyncingNetworkAPI = (*WhatsAppClient)(nil)
_ bridgev2.ChatViewingNetworkAPI = (*WhatsAppClient)(nil) _ bridgev2.ChatViewingNetworkAPI = (*WhatsAppClient)(nil)
_ bridgev2.StickerImportingNetworkAPI = (*WhatsAppClient)(nil)
) )
var pushCfg = &bridgev2.PushConfig{ 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 { func (wa *WhatsAppClient) IsThisUser(_ context.Context, userID networkid.UserID) bool {
return userID == waid.MakeUserID(wa.JID) || userID == waid.MakeUserID(wa.GetLID()) return userID == waid.MakeUserID(wa.JID)
}
func (wa *WhatsAppClient) IsOwnJID(jid types.JID) bool {
return (jid.Server == types.DefaultUserServer && jid.User == wa.JID.User) ||
(jid.Server == types.HiddenUserServer && jid.User == wa.GetLID().User)
}
func (wa *WhatsAppClient) GetLID() types.JID {
if wa.LID.IsEmpty() && !wa.JID.IsEmpty() {
wa.LID = wa.GetStore().GetLID()
}
return wa.LID
} }
func (wa *WhatsAppClient) Connect(ctx context.Context) { func (wa *WhatsAppClient) Connect(ctx context.Context) {
@ -226,7 +211,6 @@ func (wa *WhatsAppClient) Connect(ctx context.Context) {
wa.Client.BackgroundEventCtx = wa.UserLogin.Log.WithContext(wa.Main.Bridge.BackgroundCtx) wa.Client.BackgroundEventCtx = wa.UserLogin.Log.WithContext(wa.Main.Bridge.BackgroundCtx)
zerolog.Ctx(ctx).Debug().Msg("Connecting to WhatsApp") zerolog.Ctx(ctx).Debug().Msg("Connecting to WhatsApp")
if err := wa.Client.ConnectContext(ctx); err != nil { if err := wa.Client.ConnectContext(ctx); err != nil {
wa.callStopLoops()
zerolog.Ctx(ctx).Err(err).Msg("Failed to connect to WhatsApp") zerolog.Ctx(ctx).Err(err).Msg("Failed to connect to WhatsApp")
state := status.BridgeState{ state := status.BridgeState{
StateEvent: status.StateUnknownError, StateEvent: status.StateUnknownError,
@ -282,9 +266,7 @@ func (wa *WhatsAppClient) ConnectBackground(ctx context.Context, params *bridgev
return payload return payload
} }
defer func() { defer func() {
if cli := wa.Client; cli != nil { wa.Client.GetClientPayload = nil
cli.GetClientPayload = nil
}
}() }()
err := wa.Client.ConnectContext(ctx) err := wa.Client.ConnectContext(ctx)
if err != nil { if err != nil {
@ -353,7 +335,6 @@ func (wa *WhatsAppClient) startLoops() {
if oldStop != nil { if oldStop != nil {
(*oldStop)() (*oldStop)()
} }
ctx = wa.UserLogin.Log.WithContext(ctx)
go wa.historySyncLoop(ctx) go wa.historySyncLoop(ctx)
go wa.ghostResyncLoop(ctx) go wa.ghostResyncLoop(ctx)
if mrc := wa.Main.Config.HistorySync.MediaRequests; mrc.AutoRequestMedia && mrc.RequestMethod == MediaRequestMethodLocalTime { 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 return store.NoopDevice
} }
func (wa *WhatsAppClient) callStopLoops() { func (wa *WhatsAppClient) Disconnect() {
if stopHistorySyncLoop := wa.stopLoops.Swap(nil); stopHistorySyncLoop != nil { if stopHistorySyncLoop := wa.stopLoops.Swap(nil); stopHistorySyncLoop != nil {
(*stopHistorySyncLoop)() (*stopHistorySyncLoop)()
} }
}
func (wa *WhatsAppClient) Disconnect() {
wa.callStopLoops()
if cli := wa.Client; cli != nil { if cli := wa.Client; cli != nil {
cli.Disconnect() cli.Disconnect()
} }
@ -485,12 +462,3 @@ func (wa *WhatsAppClient) updatePresence(ctx context.Context, presence types.Pre
} }
return err 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
}

View file

@ -2,7 +2,6 @@ package connector
import ( import (
_ "embed" _ "embed"
"fmt"
"strings" "strings"
"text/template" "text/template"
"time" "time"
@ -71,8 +70,6 @@ type Config struct {
RequestLocalTime int `yaml:"request_local_time"` RequestLocalTime int `yaml:"request_local_time"`
MaxAsyncHandle int64 `yaml:"max_async_handle"` MaxAsyncHandle int64 `yaml:"max_async_handle"`
} `yaml:"media_requests"` } `yaml:"media_requests"`
BackwardsOnDemand bool `yaml:"backwards_on_demand"`
} `yaml:"history_sync"` } `yaml:"history_sync"`
displaynameTemplate *template.Template `yaml:"-"` displaynameTemplate *template.Template `yaml:"-"`
@ -91,15 +88,7 @@ func (c *Config) UnmarshalYAML(node *yaml.Node) error {
func (c *Config) PostProcess() error { func (c *Config) PostProcess() error {
var err error var err error
c.displaynameTemplate, err = template.New("displayname").Parse(c.DisplaynameTemplate) c.displaynameTemplate, err = template.New("displayname").Parse(c.DisplaynameTemplate)
if err != nil { return err
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
} }
func upgradeConfig(helper up.Helper) { 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.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", "request_local_time")
helper.Copy(up.Int, "history_sync", "media_requests", "max_async_handle") helper.Copy(up.Int, "history_sync", "media_requests", "max_async_handle")
helper.Copy(up.Bool, "history_sync", "backwards_on_demand")
} }
type DisplaynameParams struct { type DisplaynameParams struct {
@ -160,7 +148,7 @@ type DisplaynameParams struct {
Short string 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 var nameBuf strings.Builder
if phone == "" && jid.Server == types.DefaultUserServer { if phone == "" && jid.Server == types.DefaultUserServer {
phone = "+" + jid.User phone = "+" + jid.User
@ -179,21 +167,13 @@ func (c *Config) formatDisplayname(jid types.JID, phone string, contact types.Co
Name: contact.FullName, Name: contact.FullName,
Short: contact.FirstName, 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 { if err != nil {
panic(err) panic(err)
} }
return name return nameBuf.String()
} }
func redactPhone(phone string) string { func redactPhone(phone string) string {
if len(phone) <= 4 {
return phone
}
// This doesn't keep 2+ digit country codes properly, but whatever // This doesn't keep 2+ digit country codes properly, but whatever
return phone[:2] + strings.Repeat("∙", len(phone)-4) + phone[len(phone)-2:] return phone[:2] + strings.Repeat("∙", len(phone)-4) + phone[len(phone)-2:]
} }

View file

@ -28,24 +28,27 @@ import (
"time" "time"
"github.com/lib/pq" "github.com/lib/pq"
"github.com/rs/zerolog"
"go.mau.fi/util/dbutil" "go.mau.fi/util/dbutil"
"go.mau.fi/util/exsync"
"go.mau.fi/util/random" "go.mau.fi/util/random"
"go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/proto/waCompanionReg" "go.mau.fi/whatsmeow/proto/waCompanionReg"
"go.mau.fi/whatsmeow/store" "go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/store/sqlstore" "go.mau.fi/whatsmeow/store/sqlstore"
whatsmeowUpgrades "go.mau.fi/whatsmeow/store/sqlstore/upgrades" whatsmeowUpgrades "go.mau.fi/whatsmeow/store/sqlstore/upgrades"
"go.mau.fi/whatsmeow/types"
waLog "go.mau.fi/whatsmeow/util/log" waLog "go.mau.fi/whatsmeow/util/log"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/commands" "maunium.net/go/mautrix/bridgev2/commands"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-whatsapp/pkg/connector/wadb" "go.mau.fi/mautrix-whatsapp/pkg/connector/wadb"
"go.mau.fi/mautrix-whatsapp/pkg/msgconv" "go.mau.fi/mautrix-whatsapp/pkg/msgconv"
"go.mau.fi/mautrix-whatsapp/pkg/waid"
) )
type WhatsAppConnector struct { type WhatsAppConnector struct {
@ -61,8 +64,6 @@ type WhatsAppConnector struct {
mediaEditCache MediaEditCache mediaEditCache MediaEditCache
mediaEditCacheLock sync.RWMutex mediaEditCacheLock sync.RWMutex
stopMediaEditCacheLoop atomic.Pointer[context.CancelFunc] stopMediaEditCacheLoop atomic.Pointer[context.CancelFunc]
unmigratedDMs *exsync.Set[networkid.PortalKey]
} }
func init() { func init() {
@ -113,7 +114,6 @@ func (wa *WhatsAppConnector) Init(bridge *bridgev2.Bridge) {
cmdAccept, cmdSync, cmdInviteLink, cmdResolveLink, cmdJoin, cmdAccept, cmdSync, cmdInviteLink, cmdResolveLink, cmdJoin,
) )
wa.mediaEditCache = make(MediaEditCache) wa.mediaEditCache = make(MediaEditCache)
wa.unmigratedDMs = exsync.NewSet[networkid.PortalKey]()
whatsmeowDBLog := bridge.Log.With().Str("db_section", "whatsmeow").Logger() whatsmeowDBLog := bridge.Log.With().Str("db_section", "whatsmeow").Logger()
wa.DeviceStore = sqlstore.NewWithWrappedDB( wa.DeviceStore = sqlstore.NewWithWrappedDB(
@ -157,7 +157,78 @@ func (wa *WhatsAppConnector) Start(ctx context.Context) error {
return bridgev2.DBUpgradeError{Err: err, Section: "whatsapp"} return bridgev2.DBUpgradeError{Err: err, Section: "whatsapp"}
} }
return wa.migrateToLIDDMs(ctx) if !wa.Bridge.Background && wa.Bridge.DB.KV.Get(ctx, "whatsapp_lid_dms_deleted") == "false" {
wa.deleteLIDDMsMigration(ctx)
}
return nil
}
func (wa *WhatsAppConnector) deleteLIDDMsMigration(ctx context.Context) {
log := zerolog.Ctx(ctx).With().Str("action", "delete lid dms").Logger()
portals, err := wa.Bridge.GetAllPortalsWithMXID(ctx)
if err != nil {
log.Err(err).Msg("Failed to get portals for LID DM deletion")
return
}
defer wa.Bridge.DB.KV.Set(ctx, "whatsapp_lid_dms_deleted", "true")
if len(portals) == 0 {
log.Debug().Msg("No portals found")
return
}
portalsByKey := make(map[networkid.PortalKey]*bridgev2.Portal, len(portals))
for _, p := range portals {
if p.Receiver == "" || p.RoomType != database.RoomTypeDM {
continue
}
portalsByKey[p.PortalKey] = p
}
_, err = wa.DB.Exec(ctx, "DELETE FROM whatsapp_history_sync_conversation WHERE chat_jid LIKE '%@lid'")
if err != nil {
log.Err(err).Msg("Failed to remove LID conversations from history sync")
}
for key, portal := range portalsByKey {
parsedID, err := waid.ParsePortalID(key.ID)
if err != nil {
log.Warn().Err(err).Str("portal_id", string(key.ID)).Msg("Failed to parse portal ID")
continue
} else if parsedID.Server != types.HiddenUserServer {
continue
}
var pnStr string
err = wa.DB.QueryRow(ctx, "SELECT pn FROM whatsmeow_lid_map WHERE lid=$1", parsedID.User).Scan(&pnStr)
if err != nil {
log.Warn().Err(err).Str("portal_id", string(key.ID)).Msg("Failed to get PN for LID portal")
continue
}
key.ID = waid.MakePortalID(types.JID{User: pnStr, Server: types.DefaultUserServer})
_, pnPortalExists := portalsByKey[key]
if !pnPortalExists {
log.Warn().Str("portal_id", string(key.ID)).Msg("PN portal does not exist, not deleting LID DM")
continue
}
err = portal.Delete(ctx)
if err != nil {
log.Err(err).
Object("portal_key", portal.PortalKey).
Stringer("portal_mxid", portal.MXID).
Msg("Failed to delete LID DM portal from database")
continue
}
err = wa.Bridge.Bot.DeleteRoom(ctx, portal.MXID, false)
if err != nil {
log.Err(err).
Object("portal_key", portal.PortalKey).
Stringer("portal_mxid", portal.MXID).
Msg("Failed to delete LID DM portal from Matrix")
continue
}
log.Debug().
Object("portal_key", portal.PortalKey).
Stringer("portal_mxid", portal.MXID).
Msg("Deleted LID DM portal")
}
log.Info().Msg("Finished deleting LID DM portals")
} }
func (wa *WhatsAppConnector) Stop() { func (wa *WhatsAppConnector) Stop() {

View file

@ -17,7 +17,6 @@
package connector package connector
import ( import (
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
@ -52,7 +51,7 @@ func (wa *WhatsAppConnector) SetUseDirectMedia() {
} }
var ErrReloadNeeded = mautrix.RespError{ var ErrReloadNeeded = mautrix.RespError{
ErrCode: "COM.BEEPER.MEDIA_RELOAD_NEEDED", ErrCode: "FI.MAU.WHATSAPP_RELOAD_NEEDED",
Err: "Media is no longer available on WhatsApp servers and must be re-requested from your phone", Err: "Media is no longer available on WhatsApp servers and must be re-requested from your phone",
StatusCode: http.StatusNotFound, StatusCode: http.StatusNotFound,
} }
@ -68,8 +67,6 @@ func (wa *WhatsAppConnector) Download(ctx context.Context, mediaID networkid.Med
return wa.downloadMessageDirectMedia(ctx, parsedID, params) return wa.downloadMessageDirectMedia(ctx, parsedID, params)
} else if parsedID.Avatar != nil { } else if parsedID.Avatar != nil {
return wa.downloadAvatarDirectMedia(ctx, parsedID, params) return wa.downloadAvatarDirectMedia(ctx, parsedID, params)
} else if parsedID.Sticker != nil {
return wa.downloadStickerDirectMedia(ctx, parsedID, params)
} else { } else {
return nil, fmt.Errorf("unexpected media ID parsing result") return nil, fmt.Errorf("unexpected media ID parsing result")
} }
@ -131,30 +128,15 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars
} }
return &mediaproxy.GetMediaResponseFile{ return &mediaproxy.GetMediaResponseFile{
Callback: func(w *os.File) (*mediaproxy.FileMeta, error) { 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 }, 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) { 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()) msg, err := wa.Bridge.DB.Message.GetFirstPartByID(ctx, parsedID.UserLogin, parsedID.Message.String())
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get message: %w", err) 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 { if err != nil {
return nil, fmt.Errorf("failed to unmarshal media keys: %w", err) 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 var ul *bridgev2.UserLogin
if parsedID.UserLogin != "" { if parsedID.UserLogin != "" {
ul = wa.Bridge.GetCachedUserLoginByID(parsedID.UserLogin) ul = wa.Bridge.GetCachedUserLoginByID(parsedID.UserLogin)
@ -195,29 +174,16 @@ func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, par
if waClient.Client == nil { if waClient.Client == nil {
return nil, fmt.Errorf("no WhatsApp client found on login") 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{ return &mediaproxy.GetMediaResponseFile{
Callback: func(f *os.File) (*mediaproxy.FileMeta, error) { Callback: func(f *os.File) (*mediaproxy.FileMeta, error) {
log := zerolog.Ctx(ctx) err := waClient.Client.DownloadToFile(ctx, keys, f)
err := waClient.Client.DownloadToFile(ctx, dm, f) if errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith403) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith410) || errors.Is(err, whatsmeow.ErrNoURLPresent) {
if keys != nil && (errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith403) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith410) || errors.Is(err, whatsmeow.ErrNoURLPresent)) { val := params["fi.mau.whatsapp.reload_media"]
val := params["com.beeper.interactive_download_request"]
if val == "false" || (!wa.Config.DirectMediaAutoRequest && val != "true") { if val == "false" || (!wa.Config.DirectMediaAutoRequest && val != "true") {
return nil, ErrReloadNeeded return nil, ErrReloadNeeded
} }
log.Trace().Msg("Media not found for direct download, requesting and waiting") 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 { if err != nil {
log.Trace().Err(err).Msg("Failed to wait for media for direct download") log.Trace().Err(err).Msg("Failed to wait for media for direct download")
return nil, err return nil, err
@ -225,29 +191,30 @@ func (wa *WhatsAppConnector) makeDirectMediaResponse(
log.Trace().Msg("Retrying download after successful retry") log.Trace().Msg("Retrying download after successful retry")
err = waClient.Client.DownloadToFile(ctx, keys, f) 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") zerolog.Ctx(ctx).Warn().Err(err).Msg("Mismatching media checksums in message. Ignoring because WhatsApp seems to ignore them too")
} else if err != nil { } else if err != nil {
return nil, err return nil, err
} }
if mimeType == "application/was" { mime := keys.MimeType
if mime == "application/was" {
if _, err := f.Seek(0, io.SeekStart); err != nil { if _, err := f.Seek(0, io.SeekStart); err != nil {
return nil, fmt.Errorf("failed to seek to start of sticker zip: %w", err) return nil, fmt.Errorf("failed to seek to start of sticker zip: %w", err)
} else if zipData, err := io.ReadAll(f); err != nil { } else if zipData, err := io.ReadAll(f); err != nil {
return nil, fmt.Errorf("failed to read sticker zip: %w", err) 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) return nil, fmt.Errorf("failed to extract animated sticker: %w %x", err, zipData)
} else if _, err := f.WriteAt(data, 0); err != nil { } else if _, err := f.WriteAt(data, 0); err != nil {
return nil, fmt.Errorf("failed to write animated sticker to file: %w", err) return nil, fmt.Errorf("failed to write animated sticker to file: %w", err)
} else if err := f.Truncate(int64(len(data))); err != nil { } else if err := f.Truncate(int64(len(data))); err != nil {
return nil, fmt.Errorf("failed to truncate animated sticker file: %w", err) return nil, fmt.Errorf("failed to truncate animated sticker file: %w", err)
} }
mimeType = "video/lottie+json" mime = "video/lottie+json"
} }
return &mediaproxy.FileMeta{ return &mediaproxy.FileMeta{
ContentType: mimeType, ContentType: mime,
}, nil }, nil
}, },
}, nil }, nil

View file

@ -39,6 +39,18 @@ import (
"go.mau.fi/mautrix-whatsapp/pkg/waid" "go.mau.fi/mautrix-whatsapp/pkg/waid"
) )
func (wa *WhatsAppClient) getPortalKeyByMessageSource(ms types.MessageSource) networkid.PortalKey {
jid := ms.Chat
if ms.IsIncomingBroadcast() {
if ms.IsFromMe {
jid = ms.BroadcastListOwner.ToNonAD()
} else {
jid = ms.Sender.ToNonAD()
}
}
return wa.makeWAPortalKey(jid)
}
type MessageInfoWrapper struct { type MessageInfoWrapper struct {
Info types.MessageInfo Info types.MessageInfo
wa *WhatsAppClient wa *WhatsAppClient
@ -49,26 +61,7 @@ func (evt *MessageInfoWrapper) ShouldCreatePortal() bool {
} }
func (evt *MessageInfoWrapper) GetPortalKey() networkid.PortalKey { func (evt *MessageInfoWrapper) GetPortalKey() networkid.PortalKey {
ms := evt.Info.MessageSource return evt.wa.getPortalKeyByMessageSource(evt.Info.MessageSource)
jid := ms.Chat
if ms.IsIncomingBroadcast() {
if ms.IsFromMe {
// TODO can this still be a phone number?
jid = ms.BroadcastListOwner.ToNonAD()
} else {
jid = ms.Sender.ToNonAD()
if jid.Server == types.DefaultUserServer && !ms.SenderAlt.IsEmpty() {
jid = ms.SenderAlt.ToNonAD()
}
}
} else if jid.Server == types.DefaultUserServer {
if !ms.IsFromMe && ms.Chat.ToNonAD() == ms.Sender.ToNonAD() && !ms.SenderAlt.IsEmpty() {
jid = ms.SenderAlt.ToNonAD()
} else if !ms.RecipientAlt.IsEmpty() {
jid = ms.RecipientAlt.ToNonAD()
}
}
return evt.wa.makeWAPortalKey(jid)
} }
func (evt *MessageInfoWrapper) AddLogContext(c zerolog.Context) zerolog.Context { func (evt *MessageInfoWrapper) AddLogContext(c zerolog.Context) zerolog.Context {
@ -79,19 +72,12 @@ func (evt *MessageInfoWrapper) GetTimestamp() time.Time {
return evt.Info.Timestamp return evt.Info.Timestamp
} }
func pickLID(main, alt types.JID) types.JID {
if main.Server == types.DefaultUserServer && alt.Server == types.HiddenUserServer {
return alt
}
return main
}
func (evt *MessageInfoWrapper) GetSender() bridgev2.EventSender { func (evt *MessageInfoWrapper) GetSender() bridgev2.EventSender {
return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, pickLID(evt.Info.Sender, evt.Info.SenderAlt)) return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, evt.Info.Sender)
} }
func (evt *MessageInfoWrapper) GetID() networkid.MessageID { func (evt *MessageInfoWrapper) GetID() networkid.MessageID {
return waid.MakeMessageIDWithAltSender(evt.Info.Chat, evt.Info.Sender, evt.Info.SenderAlt, evt.Info.ID) return waid.MakeMessageID(evt.Info.Chat, evt.Info.Sender, evt.Info.ID)
} }
func (evt *MessageInfoWrapper) GetTransactionID() networkid.TransactionID { func (evt *MessageInfoWrapper) GetTransactionID() networkid.TransactionID {
@ -105,7 +91,6 @@ type WAMessageEvent struct {
parsedMessageType string parsedMessageType string
isUndecryptableUpsertSubEvent bool isUndecryptableUpsertSubEvent bool
dontRenderEdited bool
postHandle func() postHandle func()
} }
@ -148,6 +133,14 @@ func (evt *WAMessageEvent) PreHandle(ctx context.Context, portal *bridgev2.Porta
return return
} }
meta := portal.Metadata.(*waid.PortalMetadata) meta := portal.Metadata.(*waid.PortalMetadata)
if meta.AddressingMode == types.AddressingModeLID && evt.Info.Sender.Server == types.DefaultUserServer {
evt.Info.Sender, evt.Info.SenderAlt = evt.Info.SenderAlt, evt.Info.Sender
zerolog.Ctx(ctx).Debug().
Stringer("lid", evt.Info.Sender).
Stringer("pn", evt.Info.SenderAlt).
Str("message_id", evt.Info.ID).
Msg("Forced phone number sender to LID in group message")
}
if meta.AddressingMode == types.AddressingModeLID || meta.LIDMigrationAttempted { if meta.AddressingMode == types.AddressingModeLID || meta.LIDMigrationAttempted {
return return
} }
@ -166,6 +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") log.Info().Msg("Resyncing group members as it appears to have switched to LID addressing mode")
portal.UpdateInfo(ctx, evt.wa.wrapGroupInfo(ctx, info), evt.wa.UserLogin, nil, time.Time{}) portal.UpdateInfo(ctx, evt.wa.wrapGroupInfo(ctx, info), evt.wa.UserLogin, nil, time.Time{})
log.Debug().Msg("Finished resyncing after LID change") log.Debug().Msg("Finished resyncing after LID change")
if evt.Info.Sender.Server == types.DefaultUserServer && evt.Info.SenderAlt.Server == types.HiddenUserServer {
evt.Info.Sender, evt.Info.SenderAlt = evt.Info.SenderAlt, evt.Info.Sender
log.Debug().
Stringer("new_sender", evt.Info.Sender).
Stringer("new_sender_alt", evt.Info.SenderAlt).
Msg("Overriding sender to LID after resyncing group members")
}
} }
func (evt *WAMessageEvent) PostHandle(ctx context.Context, portal *bridgev2.Portal) { func (evt *WAMessageEvent) PostHandle(ctx context.Context, portal *bridgev2.Portal) {
@ -181,15 +181,12 @@ func (evt *WAMessageEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Por
} }
var editedMsg *waE2E.Message var editedMsg *waE2E.Message
var previouslyConvertedPart *bridgev2.ConvertedMessagePart var previouslyConvertedPart *bridgev2.ConvertedMessagePart
targetMessage := evt.GetTargetMessage()
cacheMessage := targetMessage
if evt.isUndecryptableUpsertSubEvent { if evt.isUndecryptableUpsertSubEvent {
// TODO db metadata needs to be updated in this case to remove the error // TODO db metadata needs to be updated in this case to remove the error
editedMsg = evt.Message editedMsg = evt.Message
cacheMessage = evt.GetID()
} else { } else {
editedMsg = evt.Message.GetProtocolMessage().GetEditedMessage() 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) meta := existing[0].Metadata.(*waid.MessageMetadata)
if slices.Contains(meta.Edits, evt.Info.ID) { if slices.Contains(meta.Edits, evt.Info.ID) {
return nil, fmt.Errorf("%w: edit already handled", bridgev2.ErrIgnoringRemoteEvent) 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.postHandle = func() {
evt.wa.processFailedMedia(ctx, portal.PortalKey, evt.GetID(), cm, false) 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]) editPart := cm.Parts[0].ToEditPart(existing[0])
if evt.isUndecryptableUpsertSubEvent || evt.dontRenderEdited { if evt.isUndecryptableUpsertSubEvent {
if editPart.TopLevelExtra == nil { if editPart.TopLevelExtra == nil {
editPart.TopLevelExtra = make(map[string]any) 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) { 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 { if existing[0].Metadata.(*waid.MessageMetadata).Error == waid.MsgErrDecryptionFailed {
evt.wa.trackUndecryptableResolved(evt.MsgEvent) 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(). zerolog.Ctx(ctx).Debug().
Stringer("existing_mxid", existing[0].MXID). Stringer("existing_mxid", existing[0].MXID).
Msg("Received decryptable version of previously undecryptable message") Msg("Received decryptable version of previously undecryptable message")
@ -324,7 +331,8 @@ func (evt *WANowDecryptableMessage) GetType() bridgev2.RemoteEventType {
type WAUndecryptableMessage struct { type WAUndecryptableMessage struct {
*MessageInfoWrapper *MessageInfoWrapper
Type events.UnavailableType Type events.UnavailableType
Hidden bool
} }
var ( var (
@ -371,9 +379,10 @@ func (evt *WAUndecryptableMessage) ConvertMessage(ctx context.Context, portal *b
// TODO thread root for comments // TODO thread root for comments
return &bridgev2.ConvertedMessage{ return &bridgev2.ConvertedMessage{
Parts: []*bridgev2.ConvertedMessagePart{{ Parts: []*bridgev2.ConvertedMessagePart{{
Type: event.EventMessage, Type: event.EventMessage,
Content: content, Content: content,
Extra: extra, Extra: extra,
DontBridge: evt.Hidden,
DBMetadata: &waid.MessageMetadata{ DBMetadata: &waid.MessageMetadata{
SenderDeviceID: evt.Info.Sender.Device, SenderDeviceID: evt.Info.Sender.Device,
Error: waid.MsgErrDecryptionFailed, Error: waid.MsgErrDecryptionFailed,
@ -390,9 +399,7 @@ func (evt *WAUndecryptableMessage) GetStreamOrder() int64 {
type WAMediaRetry struct { type WAMediaRetry struct {
*events.MediaRetry *events.MediaRetry
wa *WhatsAppClient wa *WhatsAppClient
senderLID types.JID
chatLID types.JID
} }
func (evt *WAMediaRetry) GetType() bridgev2.RemoteEventType { func (evt *WAMediaRetry) GetType() bridgev2.RemoteEventType {
@ -400,7 +407,7 @@ func (evt *WAMediaRetry) GetType() bridgev2.RemoteEventType {
} }
func (evt *WAMediaRetry) GetPortalKey() networkid.PortalKey { func (evt *WAMediaRetry) GetPortalKey() networkid.PortalKey {
return evt.wa.makeWAPortalKey(pickLID(evt.ChatID, evt.chatLID)) return evt.wa.makeWAPortalKey(evt.ChatID)
} }
func (evt *WAMediaRetry) AddLogContext(c zerolog.Context) zerolog.Context { func (evt *WAMediaRetry) AddLogContext(c zerolog.Context) zerolog.Context {
@ -414,23 +421,16 @@ func (evt *WAMediaRetry) AddLogContext(c zerolog.Context) zerolog.Context {
func (evt *WAMediaRetry) getRealSender() types.JID { func (evt *WAMediaRetry) getRealSender() types.JID {
sender := evt.SenderID sender := evt.SenderID
if sender.IsEmpty() { if evt.FromMe {
if evt.FromMe { sender = evt.wa.JID.ToNonAD()
if evt.ChatID.Server == types.HiddenUserServer { } else if sender.IsEmpty() && (evt.ChatID.Server == types.DefaultUserServer || evt.ChatID.Server == types.BotServer) {
sender = evt.wa.GetLID().ToNonAD() sender = evt.ChatID.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()
}
} }
return sender return sender
} }
func (evt *WAMediaRetry) GetSender() bridgev2.EventSender { func (evt *WAMediaRetry) GetSender() bridgev2.EventSender {
realSender := pickLID(evt.getRealSender(), evt.senderLID) return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, evt.getRealSender())
return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, realSender)
} }
func (evt *WAMediaRetry) GetTargetMessage() networkid.MessageID { func (evt *WAMediaRetry) GetTargetMessage() networkid.MessageID {

View file

@ -121,6 +121,3 @@ history_sync:
request_local_time: 120 request_local_time: 120
# Maximum number of media request responses to handle in parallel per user. # Maximum number of media request responses to handle in parallel per user.
max_async_handle: 2 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

View file

@ -48,12 +48,11 @@ var (
_ bridgev2.DeleteChatHandlingNetworkAPI = (*WhatsAppClient)(nil) _ 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) waMsg, optionMap, err := wa.Main.MsgConv.PollStartToWhatsApp(ctx, msg.Content, msg.ReplyTo, msg.Portal)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to convert poll vote: %w", err) 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) resp, err := wa.handleConvertedMatrixMessage(ctx, &msg.MatrixMessage, waMsg, nil)
if err != nil { if err != nil {
return nil, err return nil, err
@ -68,21 +67,19 @@ func (wa *WhatsAppClient) HandleMatrixPollStart(ctx context.Context, msg *bridge
return resp, nil 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) waMsg, err := wa.Main.MsgConv.PollVoteToWhatsApp(ctx, wa.Client, msg.Content, msg.VoteTo)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to convert poll vote: %w", err) 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) 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) waMsg, req, err := wa.Main.MsgConv.ToWhatsApp(ctx, wa.Client, msg.Event, msg.Content, msg.ReplyTo, msg.ThreadRoot, msg.Portal)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to convert message: %w", err) return nil, fmt.Errorf("failed to convert message: %w", err)
} }
defer wa.mcTrack(msg, time.Now(), &retErr)
return wa.handleConvertedMatrixMessage(ctx, msg, waMsg, req) return wa.handleConvertedMatrixMessage(ctx, msg, waMsg, req)
} }
@ -103,23 +100,19 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg
if err != nil { if err != nil {
return nil, err return nil, err
} }
if chatJID.Server == types.DefaultUserServer {
zerolog.Ctx(ctx).Warn().Stringer("portal_jid", chatJID).Msg("Matrix message received in phone number portal")
}
if chatJID == types.StatusBroadcastJID && wa.Main.Config.DisableStatusBroadcastSend { if chatJID == types.StatusBroadcastJID && wa.Main.Config.DisableStatusBroadcastSend {
return nil, ErrBroadcastSendDisabled return nil, ErrBroadcastSendDisabled
} }
wrappedMsgID := waid.MakeMessageID(chatJID, wa.JID, req.ID) wrappedMsgID := waid.MakeMessageID(chatJID, wa.JID, req.ID)
wrappedMsgID2 := waid.MakeMessageID(chatJID, wa.GetLID(), req.ID) wrappedMsgID2 := waid.MakeMessageID(chatJID, wa.GetStore().GetLID(), req.ID)
msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID)) msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID))
msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID2)) msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID2))
zerolog.Ctx(ctx).Trace().Any("payload", waMsg).Msg("Outgoing message payload")
resp, err := wa.Client.SendMessage(ctx, chatJID, waMsg, *req) resp, err := wa.Client.SendMessage(ctx, chatJID, waMsg, *req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
var pickedMessageID networkid.MessageID var pickedMessageID networkid.MessageID
if resp.Sender == wa.GetLID() { if resp.Sender == wa.GetStore().GetLID() && chatJID.Server != types.DefaultUserServer {
pickedMessageID = wrappedMsgID2 pickedMessageID = wrappedMsgID2
msg.RemovePending(networkid.TransactionID(wrappedMsgID)) msg.RemovePending(networkid.TransactionID(wrappedMsgID))
} else { } else {
@ -140,17 +133,18 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg
}, nil }, nil
} }
func (wa *WhatsAppClient) PreHandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (bridgev2.MatrixReactionPreResponse, error) { func (wa *WhatsAppClient) PreHandleMatrixReaction(_ context.Context, msg *bridgev2.MatrixReaction) (bridgev2.MatrixReactionPreResponse, error) {
portalJID, err := waid.ParsePortalID(msg.Portal.ID) portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil { if err != nil {
return bridgev2.MatrixReactionPreResponse{}, fmt.Errorf("failed to parse portal ID: %w", err) return bridgev2.MatrixReactionPreResponse{}, fmt.Errorf("failed to parse portal ID: %w", err)
} else if portalJID == types.StatusBroadcastJID { } else if portalJID == types.StatusBroadcastJID {
return bridgev2.MatrixReactionPreResponse{}, ErrBroadcastReactionUnsupported return bridgev2.MatrixReactionPreResponse{}, ErrBroadcastReactionUnsupported
} }
sender := wa.GetLID() sender := wa.JID
if portalJID.Server == types.DefaultUserServer { if portalJID.Server == types.HiddenUserServer ||
zerolog.Ctx(ctx).Warn().Stringer("portal_jid", portalJID).Msg("Matrix reaction received in phone number portal") msg.Portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup ||
sender = wa.JID msg.Portal.Metadata.(*waid.PortalMetadata).AddressingMode == types.AddressingModeLID {
sender = wa.GetStore().GetLID()
} }
return bridgev2.MatrixReactionPreResponse{ return bridgev2.MatrixReactionPreResponse{
SenderID: waid.MakeUserID(sender), SenderID: waid.MakeUserID(sender),
@ -159,7 +153,7 @@ func (wa *WhatsAppClient) PreHandleMatrixReaction(ctx context.Context, msg *brid
}, nil }, 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) messageID, err := waid.ParseMessageID(msg.TargetMessage.ID)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse target message ID: %w", err) 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), SenderTimestampMS: proto.Int64(msg.Event.Timestamp),
}, },
} }
defer wa.mcTrack(msg, time.Now(), &retErr)
var req whatsmeow.SendRequestExtra var req whatsmeow.SendRequestExtra
if msg.Portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup { if msg.Portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup {
reactionMsg.EncReactionMessage, err = wa.Client.EncryptReaction(ctx, msgconv.MessageIDToInfo(ctx, wa.Client, messageID), reactionMsg.ReactionMessage) reactionMsg.EncReactionMessage, err = wa.Client.EncryptReaction(ctx, msgconv.MessageIDToInfo(wa.Client, messageID), reactionMsg.ReactionMessage)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to encrypt reaction: %w", err) return nil, fmt.Errorf("failed to encrypt reaction: %w", err)
} }
@ -198,7 +191,7 @@ func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev
}, err }, 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) messageID, err := waid.ParseMessageID(msg.TargetReaction.MessageID)
if err != nil { if err != nil {
return fmt.Errorf("failed to parse target message ID: %w", err) 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) extra.ID = types.MessageID(msg.InputTransactionID)
} }
defer wa.mcTrack(msg, time.Now(), &retErr)
resp, err := wa.Client.SendMessage(ctx, portalJID, reactionMsg, extra) resp, err := wa.Client.SendMessage(ctx, portalJID, reactionMsg, extra)
zerolog.Ctx(ctx).Trace().Any("response", resp).Msg("WhatsApp reaction response") zerolog.Ctx(ctx).Trace().Any("response", resp).Msg("WhatsApp reaction response")
return err 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) log := zerolog.Ctx(ctx)
var editID types.MessageID var editID types.MessageID
@ -252,8 +244,6 @@ func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.M
if err != nil { if err != nil {
return fmt.Errorf("failed to convert message: %w", err) 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) convertedEdit := wa.Client.BuildEdit(messageID.Chat, messageID.ID, waMsg)
if edit.OrigSender == nil { if edit.OrigSender == nil {
convertedEdit.EditedMessage.Message.ProtocolMessage.TimestampMS = proto.Int64(edit.Event.Timestamp) 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 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) log := zerolog.Ctx(ctx)
messageID, err := waid.ParseMessageID(msg.TargetMessage.ID) messageID, err := waid.ParseMessageID(msg.TargetMessage.ID)
if err != nil { 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) 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) revokeMessage := wa.Client.BuildRevoke(messageID.Chat, messageID.Sender, messageID.ID)
extra := whatsmeow.SendRequestExtra{} extra := whatsmeow.SendRequestExtra{}
@ -293,7 +282,7 @@ func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *br
return err 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) { if !receipt.ReadUpTo.After(receipt.LastRead) {
return nil return nil
} }
@ -322,7 +311,7 @@ func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *
if err != nil { if err != nil {
continue continue
} }
if wa.IsOwnJID(parsed.Sender) { if parsed.Sender.User == wa.GetStore().GetLID().User || parsed.Sender.User == wa.JID.User {
continue continue
} }
var key types.JID var key types.JID
@ -332,7 +321,6 @@ func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *
} }
messagesToRead[key] = append(messagesToRead[key], parsed.ID) messagesToRead[key] = append(messagesToRead[key], parsed.ID)
} }
defer wa.mcTrack(receipt, time.Now(), &retErr)
for messageSender, ids := range messagesToRead { for messageSender, ids := range messagesToRead {
err = wa.Client.MarkRead(ctx, ids, receipt.Receipt.Timestamp, portalJID, messageSender) err = wa.Client.MarkRead(ctx, ids, receipt.Receipt.Timestamp, portalJID, messageSender)
if err != nil { if err != nil {
@ -342,7 +330,7 @@ func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *
return err 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) portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil { if err != nil {
return err return err
@ -363,7 +351,6 @@ func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.
return nil return nil
} }
defer wa.mcTrack(msg, time.Now(), &retErr)
if wa.Main.Config.SendPresenceOnTyping { if wa.Main.Config.SendPresenceOnTyping {
err = wa.updatePresence(ctx, types.PresenceAvailable) err = wa.updatePresence(ctx, types.PresenceAvailable)
if err != nil { 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) 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) portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil { if err != nil {
return false, err 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) return false, fmt.Errorf("%w (%s)", errUnsupportedDisappearingTimer, msg.Content.Timer.Duration)
} }
defer wa.mcTrack(msg, time.Now(), &retErr)
settingTS := time.UnixMilli(msg.Event.Timestamp) settingTS := time.UnixMilli(msg.Event.Timestamp)
err = wa.Client.SetDisappearingTimer(ctx, portalJID, msg.Content.Timer.Duration, settingTS) err = wa.Client.SetDisappearingTimer(ctx, portalJID, msg.Content.Timer.Duration, settingTS)
if err != nil { if err != nil {
@ -404,11 +390,7 @@ func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg
return true, nil return true, nil
} }
func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (result *bridgev2.MatrixMembershipResult, retErr error) { func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (*bridgev2.MatrixMembershipResult, error) {
if msg.Type.IsSelf && msg.OrigSender != nil {
return nil, nil
}
portalJID, err := waid.ParsePortalID(msg.Portal.ID) portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil { if err != nil {
return nil, err 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) 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) resp, err := wa.Client.UpdateGroupParticipants(ctx, portalJID, changes, action)
if err != nil { if err != nil {
return nil, err 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 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) portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil { if err != nil {
return false, err 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") 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) err = wa.Client.SetGroupName(ctx, portalJID, msg.Content.Name)
if err != nil { if err != nil {
return false, err return false, err
@ -486,7 +466,7 @@ func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev
return true, nil 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) portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil { if err != nil {
return false, err 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") return false, fmt.Errorf("cannot set room topic for DM")
} }
defer wa.mcTrack(msg, time.Now(), &retErr)
newID := wa.Client.GenerateMessageID() newID := wa.Client.GenerateMessageID()
oldID := msg.Portal.Metadata.(*waid.PortalMetadata).TopicID oldID := msg.Portal.Metadata.(*waid.PortalMetadata).TopicID
err = wa.Client.SetGroupTopic(ctx, portalJID, oldID, newID, msg.Content.Topic) 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 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) portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil { if err != nil {
return false, err 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) avatarID, err := wa.Client.SetGroupPhoto(ctx, portalJID, data)
if err != nil { if err != nil {
return false, err return false, err
@ -602,7 +580,7 @@ func convertRoomAvatar(data []byte) ([]byte, error) {
return buf.Bytes(), nil 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) chatJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil { if err != nil {
return err return err
@ -613,16 +591,14 @@ func (wa *WhatsAppClient) HandleMute(ctx context.Context, msg *bridgev2.MatrixMu
if !muted || mutedUntil == event.MutedForever { if !muted || mutedUntil == event.MutedForever {
muteTS = nil muteTS = nil
} }
defer wa.mcTrack(msg, time.Now(), &retErr)
return wa.Client.SendAppState(ctx, appstate.BuildMuteAbs(chatJID, muted, muteTS)) 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) chatJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil { if err != nil {
return err return err
} }
defer wa.mcTrack(msg, time.Now(), &retErr)
_, isFavorite := msg.Content.Tags[event.RoomTagFavourite] _, isFavorite := msg.Content.Tags[event.RoomTagFavourite]
return wa.Client.SendAppState(ctx, appstate.BuildPin(chatJID, isFavorite)) 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 lastTS = msgs[0].Timestamp
parsed, _ := waid.ParseMessageID(msgs[0].ID) parsed, _ := waid.ParseMessageID(msgs[0].ID)
if parsed != nil { if parsed != nil {
fromMe := wa.IsOwnJID(parsed.Sender) fromMe := parsed.Sender.ToNonAD() == wa.JID.ToNonAD() || parsed.Sender.ToNonAD() == wa.GetStore().GetLID().ToNonAD()
var participant *string var participant *string
if chatJID.Server == types.GroupServer { if chatJID.Server == types.GroupServer {
participant = ptr.Ptr(parsed.Sender.String()) participant = ptr.Ptr(parsed.Sender.String())
@ -654,7 +630,7 @@ func (wa *WhatsAppClient) getLastMessageInfo(ctx context.Context, chatJID types.
return lastTS, lastKey, nil 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) chatJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil { if err != nil {
return err return err
@ -663,11 +639,10 @@ func (wa *WhatsAppClient) HandleMarkedUnread(ctx context.Context, msg *bridgev2.
if err != nil { if err != nil {
return err return err
} }
defer wa.mcTrack(msg, time.Now(), &retErr)
return wa.Client.SendAppState(ctx, appstate.BuildMarkChatAsRead(chatJID, msg.Content.Unread, lastTS, lastKey)) 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) chatJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil { if err != nil {
return err return err
@ -688,6 +663,5 @@ func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridg
if err != nil { if err != nil {
return err return err
} }
defer wa.mcTrack(msg, time.Now(), &retErr)
return wa.Client.SendAppState(ctx, appstate.BuildDeleteChat(chatJID, lastTS, lastKey, true)) return wa.Client.SendAppState(ctx, appstate.BuildDeleteChat(chatJID, lastTS, lastKey, true))
} }

View file

@ -28,6 +28,7 @@ import (
"go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/appstate" "go.mau.fi/whatsmeow/appstate"
"go.mau.fi/whatsmeow/proto/waE2E" "go.mau.fi/whatsmeow/proto/waE2E"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events" "go.mau.fi/whatsmeow/types/events"
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
@ -76,7 +77,6 @@ func init() {
func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) { func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
log := wa.UserLogin.Log log := wa.UserLogin.Log
ctx := log.WithContext(wa.Main.Bridge.BackgroundCtx) ctx := log.WithContext(wa.Main.Bridge.BackgroundCtx)
wa.MC.OnWhatsAppEvent(rawEvt)
success = true success = true
switch evt := rawEvt.(type) { switch evt := rawEvt.(type) {
@ -111,9 +111,12 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
success = wa.handleWAPin(evt) success = wa.handleWAPin(evt)
case *events.HistorySync: 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: case *events.MediaRetry:
success = wa.handleWAMediaRetry(ctx, evt) wa.phoneSeen(evt.Timestamp)
success = wa.UserLogin.QueueRemoteEvent(&WAMediaRetry{MediaRetry: evt, wa: wa}).Success
case *events.GroupInfo: case *events.GroupInfo:
success = wa.handleWAGroupInfoChange(ctx, evt) success = wa.handleWAGroupInfoChange(ctx, evt)
@ -125,20 +128,6 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
success = wa.handleWANewsletterLeave(evt) success = wa.handleWANewsletterLeave(evt)
case *events.Picture: case *events.Picture:
success = wa.handleWAPictureUpdate(ctx, evt) 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: case *events.AppStateSyncComplete:
wa.handleWAAppStateSyncComplete(ctx, evt) wa.handleWAAppStateSyncComplete(ctx, evt)
@ -157,7 +146,7 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
if err != nil { if err != nil {
log.Err(err).Msg("Failed to update push name in store") log.Err(err).Msg("Failed to update push name in store")
} }
_, _, err = wa.GetStore().Contacts.PutPushName(ctx, wa.GetLID().ToNonAD(), evt.Action.GetName()) _, _, err = wa.GetStore().Contacts.PutPushName(ctx, wa.GetStore().GetLID().ToNonAD(), evt.Action.GetName())
if err != nil { if err != nil {
log.Err(err).Msg("Failed to update push name in store") log.Err(err).Msg("Failed to update push name in store")
} }
@ -181,6 +170,7 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
}() }()
go wa.syncRemoteProfile(ctx, nil) go wa.syncRemoteProfile(ctx, nil)
} }
wa.MC.OnConnect(store.GetWAVersion()[2], wa.Device.Platform)
case *events.OfflineSyncPreview: case *events.OfflineSyncPreview:
log.Info(). log.Info().
Int("message_count", evt.Messages). Int("message_count", evt.Messages).
@ -258,40 +248,115 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
return return
} }
func (wa *WhatsAppClient) ensureAltJIDs(ctx context.Context, info *types.MessageSource, checkPhones bool) bool { func (wa *WhatsAppClient) rerouteWAMessage(ctx context.Context, evtType string, info *types.MessageSource, msgID any) {
var err error if (info.Chat.Server == types.HiddenUserServer || info.Chat.Server == types.BroadcastServer) &&
if info.Sender.Server == types.DefaultUserServer && info.SenderAlt.IsEmpty() { info.Sender.Server == types.HiddenUserServer && info.SenderAlt.IsEmpty() {
info.SenderAlt, err = wa.GetStore().LIDs.GetLIDForPN(ctx, info.Sender) info.SenderAlt, _ = wa.GetStore().LIDs.GetPNForLID(ctx, info.Sender)
}
if info.Chat.Server == types.HiddenUserServer && info.IsFromMe && info.RecipientAlt.IsEmpty() {
info.RecipientAlt, _ = wa.GetStore().LIDs.GetPNForLID(ctx, info.Chat)
}
if info.Chat.Server == types.HiddenUserServer && info.Sender.ToNonAD() == info.Chat && info.SenderAlt.Server == types.DefaultUserServer {
wa.UserLogin.Log.Debug().
Stringer("lid", info.Sender).
Stringer("pn", info.SenderAlt).
Any("message_id", msgID).
Str("evt_type", evtType).
Msg("Forced LID DM sender to phone number in incoming message")
info.Sender, info.SenderAlt = info.SenderAlt, info.Sender
info.Chat = info.Sender.ToNonAD()
} else if info.Chat.Server == types.HiddenUserServer && info.IsFromMe && info.RecipientAlt.Server == types.DefaultUserServer {
wa.UserLogin.Log.Debug().
Stringer("lid", info.Chat).
Stringer("pn", info.RecipientAlt).
Any("message_id", msgID).
Str("evt_type", evtType).
Msg("Forced LID DM sender to phone number in own message sent from another device")
info.Chat = info.RecipientAlt.ToNonAD()
if info.Sender.Server == types.HiddenUserServer {
info.Sender, info.SenderAlt = info.SenderAlt, info.Sender
if info.Sender.IsEmpty() {
info.Sender = wa.GetStore().GetJID()
info.Sender.Device = info.SenderAlt.Device
}
}
} else if info.Chat.Server == types.BroadcastServer && info.Sender.Server == types.HiddenUserServer && info.SenderAlt.Server == types.DefaultUserServer {
wa.UserLogin.Log.Debug().
Stringer("lid", info.Sender).
Stringer("pn", info.SenderAlt).
Stringer("chat", info.Chat).
Any("message_id", msgID).
Str("evt_type", evtType).
Msg("Forced LID broadcast list sender to phone number in incoming message")
info.Sender, info.SenderAlt = info.SenderAlt, info.Sender
} else if info.Sender.Server == types.BotServer && info.Chat.Server == types.HiddenUserServer {
chatPN, err := wa.GetStore().LIDs.GetPNForLID(ctx, info.Chat)
if err != nil { if err != nil {
zerolog.Ctx(ctx).Err(err).Stringer("sender", info.Sender).Msg("Failed to get LID for sender") wa.UserLogin.Log.Err(err).
return false Any("message_id", msgID).
Stringer("lid", info.Chat).
Str("evt_type", evtType).
Msg("Failed to get phone number of DM for incoming bot message")
} else if !chatPN.IsEmpty() {
wa.UserLogin.Log.Debug().
Stringer("lid", info.Chat).
Stringer("pn", chatPN).
Any("message_id", msgID).
Str("evt_type", evtType).
Msg("Forced LID chat to phone number in bot message")
info.Chat = chatPN
} }
} }
if info.Chat.Server == types.DefaultUserServer && info.IsFromMe && info.RecipientAlt.IsEmpty() {
info.RecipientAlt, err = wa.GetStore().LIDs.GetLIDForPN(ctx, info.Chat)
if err != nil {
zerolog.Ctx(ctx).Err(err).Stringer("chat", info.Chat).Msg("Failed to get LID for chat")
return false
}
}
if checkPhones {
return wa.checkAllPhonesInMessage(ctx, info)
}
return true
} }
func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Message) (success bool) { func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Message) (success bool) {
success = true 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 { if evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast {
return 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 return
} }
if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, true) {
return false
}
parsedMessageType := getMessageType(evt.Message) parsedMessageType := getMessageType(evt.Message)
if parsedMessageType == "ignore" || strings.HasPrefix(parsedMessageType, "unknown_protocol_") {
return
}
if encReact := evt.Message.GetEncReactionMessage(); encReact != nil { if encReact := evt.Message.GetEncReactionMessage(); encReact != nil {
decrypted, err := wa.Client.DecryptReaction(ctx, evt) decrypted, err := wa.Client.DecryptReaction(ctx, evt)
if err != nil { if err != nil {
@ -313,65 +378,13 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
if encMessage := evt.Message.GetSecretEncryptedMessage(); encMessage != nil { if encMessage := evt.Message.GetSecretEncryptedMessage(); encMessage != nil {
decrypted, err := wa.Client.DecryptSecretEncryptedMessage(ctx, evt) decrypted, err := wa.Client.DecryptSecretEncryptedMessage(ctx, evt)
if err != nil { if err != nil {
wa.UserLogin.Log.Err(err). wa.UserLogin.Log.Err(err).Str("message_id", evt.Info.ID).Msg("Failed to decrypt message")
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")
return return
} }
evt.RawMessage = decrypted evt.RawMessage = decrypted
evt.UnwrapRaw() evt.UnwrapRaw()
parsedMessageType = getMessageType(evt.Message) 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{ res := wa.UserLogin.QueueRemoteEvent(&WAMessageEvent{
MessageInfoWrapper: &MessageInfoWrapper{ MessageInfoWrapper: &MessageInfoWrapper{
Info: evt.Info, Info: evt.Info,
@ -381,75 +394,39 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
MsgEvent: evt, MsgEvent: evt,
parsedMessageType: parsedMessageType, parsedMessageType: parsedMessageType,
dontRenderEdited: dontRenderEdited,
}) })
return res.Success return res.Success
} }
func (wa *WhatsAppClient) handleWAUndecryptableMessage(ctx context.Context, evt *events.UndecryptableMessage) bool { func (wa *WhatsAppClient) handleWAUndecryptableMessage(ctx context.Context, evt *events.UndecryptableMessage) bool {
if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, true) { wa.rerouteWAMessage(ctx, "undecryptable message", &evt.Info.MessageSource, evt.Info.ID)
return false
}
wa.UserLogin.Log.Debug(). wa.UserLogin.Log.Debug().
Any("info", evt.Info). Any("info", evt.Info).
Bool("unavailable", evt.IsUnavailable). Bool("unavailable", evt.IsUnavailable).
Str("decrypt_fail", string(evt.DecryptFailMode)). Str("decrypt_fail", string(evt.DecryptFailMode)).
Msg("Received undecryptable WhatsApp message") Msg("Received undecryptable WhatsApp message")
wa.trackUndecryptable(evt) wa.trackUndecryptable(evt)
if evt.DecryptFailMode == events.DecryptFailHide {
return true
}
if evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast { if evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast {
return true return true
} }
hidden := evt.DecryptFailMode == events.DecryptFailHide
res := wa.UserLogin.QueueRemoteEvent(&WAUndecryptableMessage{ res := wa.UserLogin.QueueRemoteEvent(&WAUndecryptableMessage{
MessageInfoWrapper: &MessageInfoWrapper{ MessageInfoWrapper: &MessageInfoWrapper{
Info: evt.Info, Info: evt.Info,
wa: wa, wa: wa,
}, },
Type: evt.UnavailableType, Type: evt.UnavailableType,
}) Hidden: hidden,
return res.Success
}
func (wa *WhatsAppClient) handleWAMediaRetry(ctx context.Context, evt *events.MediaRetry) bool {
wa.phoneSeen(evt.Timestamp)
var senderLID, chatLID types.JID
var err error
if evt.SenderID.Server == types.DefaultUserServer {
senderLID, err = wa.GetStore().LIDs.GetLIDForPN(ctx, evt.SenderID)
if err != nil {
wa.UserLogin.Log.Err(err).
Stringer("sender_id", evt.SenderID).
Msg("Failed to get LID for media retry sender")
return false
}
}
if evt.ChatID.Server == types.DefaultUserServer {
chatLID, err = wa.GetStore().LIDs.GetLIDForPN(ctx, evt.ChatID)
if err != nil {
wa.UserLogin.Log.Err(err).
Stringer("chat_id", evt.ChatID).
Msg("Failed to get LID for media retry chat")
return false
}
}
res := wa.UserLogin.QueueRemoteEvent(&WAMediaRetry{
MediaRetry: evt,
wa: wa,
senderLID: senderLID,
chatLID: chatLID,
}) })
return res.Success return res.Success
} }
func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Receipt) (success bool) { func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Receipt) (success bool) {
origChat := evt.Chat
wa.rerouteWAMessage(ctx, "receipt", &evt.MessageSource, evt.MessageIDs)
if evt.IsFromMe && evt.Sender.Device == 0 { if evt.IsFromMe && evt.Sender.Device == 0 {
wa.phoneSeen(evt.Timestamp) wa.phoneSeen(evt.Timestamp)
} }
if !wa.ensureAltJIDs(ctx, &evt.MessageSource, true) {
return false
}
var evtType bridgev2.RemoteEventType var evtType bridgev2.RemoteEventType
switch evt.Type { switch evt.Type {
case types.ReceiptTypeRead, types.ReceiptTypeReadSelf: case types.ReceiptTypeRead, types.ReceiptTypeReadSelf:
@ -461,34 +438,28 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei
default: default:
return true return true
} }
targets := make([]networkid.MessageID, 0, len(evt.MessageIDs)) targets := make([]networkid.MessageID, len(evt.MessageIDs))
messageSender := wa.GetLID() messageSender := wa.JID
if !evt.MessageSender.IsEmpty() { if !evt.MessageSender.IsEmpty() {
messageSender = evt.MessageSender messageSender = evt.MessageSender
} // Second part of rerouting receipts in LID chats
var chatAlt types.JID if messageSender == origChat && evt.Chat != origChat {
if evt.Chat.Server == types.DefaultUserServer { messageSender = evt.Chat
chatLID, _ := wa.GetStore().LIDs.GetLIDForPN(ctx, evt.Chat) }
if !chatLID.IsEmpty() { } else if evt.Chat.Server == types.GroupServer && evt.Sender.Server == types.HiddenUserServer {
chatAlt = evt.Chat lid := wa.GetStore().GetLID()
evt.Chat = chatLID if !lid.IsEmpty() {
messageSender = lid
} }
} }
for _, id := range evt.MessageIDs { for i, id := range evt.MessageIDs {
targets = append(targets, waid.MakeMessageID(evt.Chat, messageSender, id)) targets[i] = waid.MakeMessageID(evt.Chat, messageSender, id)
if !chatAlt.IsEmpty() {
targets = append(targets, waid.MakeMessageID(chatAlt, messageSender, id))
}
}
senderLID := evt.Sender
if senderLID.Server == types.DefaultUserServer && !evt.SenderAlt.IsEmpty() {
senderLID = evt.SenderAlt
} }
res := wa.UserLogin.QueueRemoteEvent(&simplevent.Receipt{ res := wa.UserLogin.QueueRemoteEvent(&simplevent.Receipt{
EventMeta: simplevent.EventMeta{ EventMeta: simplevent.EventMeta{
Type: evtType, Type: evtType,
PortalKey: wa.makeWAPortalKey(evt.Chat), PortalKey: wa.makeWAPortalKey(evt.Chat),
Sender: wa.makeEventSender(ctx, senderLID), Sender: wa.makeEventSender(ctx, evt.Sender),
Timestamp: evt.Timestamp, Timestamp: evt.Timestamp,
}, },
Targets: targets, Targets: targets,
@ -497,11 +468,11 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei
} }
func (wa *WhatsAppClient) handleWAChatPresence(ctx context.Context, evt *events.ChatPresence) { func (wa *WhatsAppClient) handleWAChatPresence(ctx context.Context, evt *events.ChatPresence) {
if evt.Chat.Server == types.DefaultUserServer && evt.Sender.ToNonAD() == evt.Chat { if evt.Chat.Server == types.HiddenUserServer && evt.Sender.ToNonAD() == evt.Chat {
if evt.SenderAlt.IsEmpty() { if evt.SenderAlt.IsEmpty() {
evt.SenderAlt, _ = wa.GetStore().LIDs.GetLIDForPN(ctx, evt.Sender) evt.SenderAlt, _ = wa.GetStore().LIDs.GetPNForLID(ctx, evt.Sender)
} }
if evt.SenderAlt.Server == types.HiddenUserServer { if evt.SenderAlt.Server == types.DefaultUserServer {
evt.Sender, evt.SenderAlt = evt.SenderAlt, evt.Sender evt.Sender, evt.SenderAlt = evt.SenderAlt, evt.Sender
evt.Chat = evt.Sender.ToNonAD() evt.Chat = evt.Sender.ToNonAD()
} }
@ -535,10 +506,9 @@ func (wa *WhatsAppClient) handleWALogout(reason events.ConnectFailureReason, onC
} else if reason == events.ConnectFailureMainDeviceGone { } else if reason == events.ConnectFailureMainDeviceGone {
errorCode = WAMainDeviceGone errorCode = WAMainDeviceGone
} }
wa.Disconnect() wa.Client.Disconnect()
wa.Client = nil wa.Client = nil
wa.JID = types.EmptyJID wa.JID = types.EmptyJID
wa.LID = types.EmptyJID
wa.UserLogin.Metadata.(*waid.UserLoginMetadata).WADeviceID = 0 wa.UserLogin.Metadata.(*waid.UserLoginMetadata).WADeviceID = 0
wa.UserLogin.BridgeState.Send(status.BridgeState{ wa.UserLogin.BridgeState.Send(status.BridgeState{
StateEvent: status.StateBadCredentials, StateEvent: status.StateBadCredentials,
@ -552,15 +522,12 @@ func (wa *WhatsAppClient) handleWACallStart(ctx context.Context, group, sender,
if !wa.Main.Config.CallStartNotices || time.Since(ts) > callEventMaxAge { if !wa.Main.Config.CallStartNotices || time.Since(ts) > callEventMaxAge {
return true return true
} }
if sender.Server == types.DefaultUserServer && senderAlt.IsEmpty() { if sender.Server == types.HiddenUserServer && senderAlt.Server == types.DefaultUserServer {
senderAlt, _ = wa.GetStore().LIDs.GetLIDForPN(ctx, sender)
}
if sender.Server == types.DefaultUserServer && senderAlt.Server == types.HiddenUserServer {
wa.UserLogin.Log.Debug(). wa.UserLogin.Log.Debug().
Stringer("lid", senderAlt). Stringer("lid", sender).
Stringer("pn", sender). Stringer("pn", senderAlt).
Str("call_id", id). Str("call_id", id).
Msg("Forced phone number caller to LID in incoming call") Msg("Forced LID caller to phone number in incoming call")
sender, senderAlt = senderAlt, sender sender, senderAlt = senderAlt, sender
} }
chat := group chat := group
@ -607,12 +574,6 @@ func (wa *WhatsAppClient) handleWAIdentityChange(ctx context.Context, evt *event
if !wa.Main.Config.IdentityChangeNotices { if !wa.Main.Config.IdentityChangeNotices {
return return
} }
if evt.JID.Server == types.DefaultUserServer {
lid, _ := wa.GetStore().LIDs.GetLIDForPN(ctx, evt.JID)
if !lid.IsEmpty() {
evt.JID = lid
}
}
wa.UserLogin.QueueRemoteEvent(&simplevent.Message[*events.IdentityChange]{ wa.UserLogin.QueueRemoteEvent(&simplevent.Message[*events.IdentityChange]{
EventMeta: simplevent.EventMeta{ EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventMessage, Type: bridgev2.RemoteEventMessage,
@ -663,14 +624,13 @@ func (wa *WhatsAppClient) handleWADeleteChat(ctx context.Context, evt *events.De
func (wa *WhatsAppClient) handleWADeleteForMe(ctx context.Context, evt *events.DeleteForMe) bool { func (wa *WhatsAppClient) handleWADeleteForMe(ctx context.Context, evt *events.DeleteForMe) bool {
chatJID := wa.maybeConvertJIDToLID(ctx, evt.ChatJID) chatJID := wa.maybeConvertJIDToLID(ctx, evt.ChatJID)
senderJID := wa.maybeConvertJIDToLID(ctx, evt.SenderJID)
return wa.UserLogin.QueueRemoteEvent(&simplevent.MessageRemove{ return wa.UserLogin.QueueRemoteEvent(&simplevent.MessageRemove{
EventMeta: simplevent.EventMeta{ EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventMessageRemove, Type: bridgev2.RemoteEventMessageRemove,
PortalKey: wa.makeWAPortalKey(chatJID), PortalKey: wa.makeWAPortalKey(chatJID),
Timestamp: evt.Timestamp, Timestamp: evt.Timestamp,
}, },
TargetMessage: waid.MakeMessageID(chatJID, senderJID, evt.MessageID), TargetMessage: waid.MakeMessageID(chatJID, evt.SenderJID, evt.MessageID),
OnlyForMe: true, OnlyForMe: true,
}).Success }).Success
} }
@ -681,7 +641,7 @@ func (wa *WhatsAppClient) handleWAMarkChatAsRead(ctx context.Context, evt *event
EventMeta: simplevent.EventMeta{ EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventReadReceipt, Type: bridgev2.RemoteEventReadReceipt,
PortalKey: wa.makeWAPortalKey(chatJID), PortalKey: wa.makeWAPortalKey(chatJID),
Sender: wa.makeEventSender(ctx, wa.GetLID()), Sender: wa.makeEventSender(ctx, wa.JID),
Timestamp: evt.Timestamp, Timestamp: evt.Timestamp,
}, },
ReadUpTo: evt.Timestamp, ReadUpTo: evt.Timestamp,
@ -770,6 +730,9 @@ func (wa *WhatsAppClient) handleWAGroupInfoChange(ctx context.Context, evt *even
} }
func (wa *WhatsAppClient) handleWAJoinedGroup(ctx context.Context, evt *events.JoinedGroup) bool { func (wa *WhatsAppClient) handleWAJoinedGroup(ctx context.Context, evt *events.JoinedGroup) bool {
if wa.createDedup.Pop(evt.CreateKey) {
return true
}
return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{ return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{
EventMeta: simplevent.EventMeta{ EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventChatResync, Type: bridgev2.RemoteEventChatResync,
@ -782,9 +745,6 @@ func (wa *WhatsAppClient) handleWAJoinedGroup(ctx context.Context, evt *events.J
} }
func (wa *WhatsAppClient) handleWANewsletterJoin(ctx context.Context, evt *events.NewsletterJoin) bool { func (wa *WhatsAppClient) handleWANewsletterJoin(ctx context.Context, evt *events.NewsletterJoin) bool {
if wa.disableNewsletter {
return true
}
return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{ return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{
EventMeta: simplevent.EventMeta{ EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventChatResync, Type: bridgev2.RemoteEventChatResync,
@ -883,12 +843,7 @@ func (wa *WhatsAppClient) handleWAAppStateSyncComplete(ctx context.Context, evt
} else { } else {
log.Info(). log.Info().
Time("recovery_ts", ts). Time("recovery_ts", ts).
Bool("recovery_evt", evt.Recovery).
Msg("Unmarked app state recovery as attempted after successful full sync") Msg("Unmarked app state recovery as attempted after successful full sync")
wa.UserLogin.TrackAnalytics("WhatsApp Appstate Recovery Success", map[string]any{
"patch_name": evt.Name,
"from_recovery": evt.Recovery,
})
} }
} else if ts, exists = wa.appStateFullSyncAttempted[evt.Name]; exists { } else if ts, exists = wa.appStateFullSyncAttempted[evt.Name]; exists {
delete(wa.appStateFullSyncAttempted, evt.Name) delete(wa.appStateFullSyncAttempted, evt.Name)
@ -945,9 +900,6 @@ func (wa *WhatsAppClient) handleWAAppStateSyncError(ctx context.Context, evt *ev
if err != nil { if err != nil {
log.Err(err).Msg("Failed to save login metadata after marking app state recovery as attempted") log.Err(err).Msg("Failed to save login metadata after marking app state recovery as attempted")
} }
wa.UserLogin.TrackAnalytics("WhatsApp Appstate Recovery Request", map[string]any{
"patch_name": evt.Name,
})
go func() { go func() {
resp, err := wa.Client.SendPeerMessage(ctx, whatsmeow.BuildAppStateRecoveryRequest(evt.Name)) resp, err := wa.Client.SendPeerMessage(ctx, whatsmeow.BuildAppStateRecoveryRequest(evt.Name))
if err != nil { if err != nil {

View file

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

View file

@ -1,212 +0,0 @@
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
// Copyright (C) 2026 Tulir Asokan
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <https://www.gnu.org/licenses/>.
package connector
import (
"context"
"fmt"
"time"
"github.com/rs/zerolog"
"go.mau.fi/util/dbutil"
"go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event"
"go.mau.fi/mautrix-whatsapp/pkg/waid"
)
func (wa *WhatsAppClient) FindAltTargetMessage(ctx context.Context, targetMsg networkid.MessageID, evt bridgev2.RemoteEventWithTargetMessage) (alts []networkid.MessageID, err error) {
parsed, err := waid.ParseMessageID(targetMsg)
if err != nil {
return nil, fmt.Errorf("failed to parse target message ID: %w", err)
}
altSender, err := wa.GetStore().GetAltJID(ctx, parsed.Sender)
if err != nil {
return nil, err
}
var altChat types.JID
if parsed.Chat.Server == types.HiddenUserServer {
altChat, err = wa.GetStore().LIDs.GetPNForLID(ctx, parsed.Chat)
if err != nil {
return nil, err
}
}
if !altSender.IsEmpty() {
altSenderID := *parsed
altSenderID.Sender = altSender
alts = append(alts, altSenderID.String())
}
if !altChat.IsEmpty() {
altChatID := *parsed
altChatID.Chat = altChat
if altSender.Server == types.DefaultUserServer {
altChatID.Sender = altSender
}
alts = append(alts, altChatID.String())
}
return
}
func (wa *WhatsAppClient) checkAllPhonesInMessage(ctx context.Context, info *types.MessageSource) (ok bool) {
for _, jid := range []types.JID{info.Sender, info.SenderAlt, info.Chat, info.RecipientAlt, info.BroadcastListOwner} {
if !wa.reIDPhoneDMToLIDIfNeeded(ctx, jid) {
return false
}
}
return true
}
func (wa *WhatsAppClient) reIDPhoneDMToLIDIfNeeded(ctx context.Context, pn types.JID) (ok bool) {
if pn.Server != types.DefaultUserServer {
return true
}
portalKey := wa.makeWAPortalKey(pn)
if wa.Main.unmigratedDMs.Has(portalKey) {
lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, pn)
if err != nil {
zerolog.Ctx(ctx).Err(err).Stringer("pn", pn).Msg("Failed to get LID for PN")
return false
} else if lid.IsEmpty() {
zerolog.Ctx(ctx).Warn().Stringer("pn", pn).Msg("No found LID for phone number")
return true
}
zerolog.Ctx(ctx).Info().
Object("portal_key", portalKey).
Stringer("pn", pn).
Stringer("lid", lid).
Msg("Received event for portal in unmigrated DMs list, trying migration")
_, err = wa.Main.reIDPhoneDMToLID(ctx, pn, lid, wa.UserLogin.ID)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to re-ID phone DM to LID")
return false
}
}
return true
}
func (wa *WhatsAppConnector) reIDPhoneDMToLID(ctx context.Context, pn, lid types.JID, receiver networkid.UserLoginID) (bridgev2.ReIDResult, error) {
pnKey := networkid.PortalKey{
ID: waid.MakePortalID(pn),
Receiver: receiver,
}
lidKey := networkid.PortalKey{
ID: waid.MakePortalID(lid),
Receiver: receiver,
}
result, portal, err := wa.Bridge.ReIDPortal(ctx, pnKey, lidKey)
if err != nil {
return result, err
}
if result == bridgev2.ReIDResultSourceReIDd || result == bridgev2.ReIDResultTargetDeletedAndSourceReIDd {
var pnGhost, lidGhost *bridgev2.Ghost
pnGhost, err = wa.Bridge.GetGhostByID(ctx, waid.MakeUserID(pn))
if err != nil {
return result, fmt.Errorf("failed to get PN ghost: %w", err)
}
lidGhost, err = wa.Bridge.GetGhostByID(ctx, waid.MakeUserID(lid))
if err != nil {
return result, fmt.Errorf("failed to get LID ghost: %w", err)
}
_, err = pnGhost.Intent.SendState(ctx, portal.MXID, event.StateMember, pnGhost.Intent.GetMXID().String(), &event.Content{
Parsed: &event.MemberEventContent{Membership: event.MembershipLeave, Reason: "Migrating to LIDs"},
Raw: map[string]any{"com.beeper.exclude_from_timeline": true},
}, time.Time{})
if err != nil {
return result, fmt.Errorf("failed to send leave event for PN ghost: %w", err)
}
_, err = wa.Bridge.Bot.SendState(ctx, portal.MXID, event.StateMember, lidGhost.Intent.GetMXID().String(), &event.Content{
Parsed: &event.MemberEventContent{Membership: event.MembershipInvite, Reason: "Migrating to LIDs"},
Raw: map[string]any{"com.beeper.exclude_from_timeline": true},
}, time.Time{})
if err != nil {
return result, fmt.Errorf("failed to send invite event for LID ghost: %w", err)
}
_, err = lidGhost.Intent.SendState(ctx, portal.MXID, event.StateMember, lidGhost.Intent.GetMXID().String(), &event.Content{
Parsed: &event.MemberEventContent{Membership: event.MembershipJoin, Reason: "Migrating to LIDs"},
Raw: map[string]any{"com.beeper.exclude_from_timeline": true},
}, time.Time{})
if err != nil {
return result, fmt.Errorf("failed to send join event for LID ghost: %w", err)
}
portal.OtherUserID = lidGhost.ID
err = portal.Save(ctx)
if err != nil {
return result, fmt.Errorf("failed to save portal after re-ID: %w", err)
}
portal.UpdateBridgeInfo(ctx)
}
return result, nil
}
var scanPortalKey = dbutil.ConvertRowFn[networkid.PortalKey](func(row dbutil.Scannable) (key networkid.PortalKey, err error) {
err = row.Scan(&key.ID, &key.Receiver)
return
})
func (wa *WhatsAppConnector) migrateToLIDDMs(ctx context.Context) error {
if wa.Bridge.Background {
if wa.Bridge.DB.KV.Get(ctx, "whatsapp_lid_dms_migrated") == "true" {
return nil
}
return fmt.Errorf("can't migrate to LID DMs in background mode")
}
log := zerolog.Ctx(ctx).With().Str("action", "migrate to lid dms").Logger()
const findPNPortals = "SELECT id, receiver FROM portal WHERE bridge_id=$1 AND room_type='dm' AND id LIKE '%@s.whatsapp.net'"
pnPortalKeys, err := scanPortalKey.NewRowIter(wa.Bridge.DB.Query(ctx, findPNPortals, wa.Bridge.ID)).AsList()
if err != nil {
return fmt.Errorf("failed to get phone number portals: %w", err)
}
var updatedPortals, missingLID int
for _, key := range pnPortalKeys {
pnJID, err := waid.ParsePortalID(key.ID)
if err != nil {
log.Warn().Err(err).Str("portal_id", string(key.ID)).Msg("Failed to parse portal ID")
continue
} else if pnJID.Server != types.DefaultUserServer {
continue
}
lid, err := wa.DeviceStore.LIDMap.GetLIDForPN(ctx, pnJID)
if err != nil {
return fmt.Errorf("failed to get LID for PN portal %s: %w", key.ID, err)
} else if lid.IsEmpty() {
log.Warn().Stringer("pn", pnJID).Msg("No LID for PN portal")
wa.unmigratedDMs.Add(key)
missingLID++
continue
}
res, err := wa.reIDPhoneDMToLID(ctx, pnJID, lid, key.Receiver)
if err != nil {
return fmt.Errorf("failed to re-ID %s to %s: %w", pnJID, lid, err)
}
updatedPortals++
log.Info().
Stringer("pn", pnJID).
Stringer("lid", lid).
Stringer("result", res).
Msg("Re-ID'd phone number DM portal")
}
log.Info().
Int("updated_portals", updatedPortals).
Int("total_pn_portals", len(pnPortalKeys)).
Int("missing_lid", missingLID).
Msg("Finished re-IDing phone number DM portals")
wa.Bridge.DB.KV.Set(ctx, "whatsapp_lid_dms_migrated", "true")
return nil
}

View file

@ -2,7 +2,6 @@ package connector
import ( import (
"context" "context"
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"net/http" "net/http"
@ -13,7 +12,6 @@ import (
"go.mau.fi/util/exsync" "go.mau.fi/util/exsync"
"go.mau.fi/util/jsontime" "go.mau.fi/util/jsontime"
"go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events" "go.mau.fi/whatsmeow/types/events"
waLog "go.mau.fi/whatsmeow/util/log" waLog "go.mau.fi/whatsmeow/util/log"
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
@ -27,7 +25,6 @@ const (
LoginStepIDQR = "fi.mau.whatsapp.login.qr" LoginStepIDQR = "fi.mau.whatsapp.login.qr"
LoginStepIDPhoneNumber = "fi.mau.whatsapp.login.phone" LoginStepIDPhoneNumber = "fi.mau.whatsapp.login.phone"
LoginStepIDCode = "fi.mau.whatsapp.login.code" LoginStepIDCode = "fi.mau.whatsapp.login.code"
LoginStepIDPasskey = "fi.mau.whatsapp.login.passkey"
LoginStepIDComplete = "fi.mau.whatsapp.login.complete" LoginStepIDComplete = "fi.mau.whatsapp.login.complete"
LoginFlowIDQR = "qr" LoginFlowIDQR = "qr"
@ -79,8 +76,8 @@ var (
} }
ErrRateLimitedByWhatsApp = bridgev2.RespError{ ErrRateLimitedByWhatsApp = bridgev2.RespError{
ErrCode: "FI.MAU.WHATSAPP.RATE_LIMITED", ErrCode: "FI.MAU.WHATSAPP.RATE_LIMITED",
Err: "Rate limited by WhatsApp. Try again later.", Err: "Rate limited by WhatsApp",
StatusCode: http.StatusBadRequest, StatusCode: http.StatusTooManyRequests,
} }
) )
@ -94,11 +91,9 @@ func (wa *WhatsAppConnector) CreateLogin(_ context.Context, user *bridgev2.User,
Bool("phone_code", flowID == LoginFlowIDPhone). Bool("phone_code", flowID == LoginFlowIDPhone).
Logger(), Logger(),
WaitForQRs: exsync.NewEvent(), WaitForQRs: exsync.NewEvent(),
LoginComplete: exsync.NewEvent(), LoginComplete: exsync.NewEvent(),
PasskeyRequest: exsync.NewEvent(), Received515: exsync.NewEvent(),
PasskeyConfirmation: exsync.NewEvent(),
Received515: exsync.NewEvent(),
}, nil }, nil
} }
@ -119,11 +114,6 @@ type WALogin struct {
Received515 *exsync.Event Received515 *exsync.Event
PrevQRIndex atomic.Int32 PrevQRIndex atomic.Int32
PasskeyRequest *exsync.Event
PasskeyRequestData *events.PairPasskeyRequest
PasskeyConfirmation *exsync.Event
PasskeyConfirmationData *events.PairPasskeyConfirmation
Closed atomic.Bool Closed atomic.Bool
EventHandlerID uint32 EventHandlerID uint32
} }
@ -132,11 +122,9 @@ var (
_ bridgev2.LoginProcessDisplayAndWait = (*WALogin)(nil) _ bridgev2.LoginProcessDisplayAndWait = (*WALogin)(nil)
_ bridgev2.LoginProcessUserInput = (*WALogin)(nil) _ bridgev2.LoginProcessUserInput = (*WALogin)(nil)
_ bridgev2.LoginProcessWithOverride = (*WALogin)(nil) _ bridgev2.LoginProcessWithOverride = (*WALogin)(nil)
_ bridgev2.LoginProcessWebAuthn = (*WALogin)(nil)
) )
const LoginConnectWait = 30 * time.Second const LoginConnectWait = 15 * time.Second
const LoginPairPhoneWait = 30 * time.Second
func (wl *WALogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) { func (wl *WALogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) {
wl.Main.firstClientConnectOnce.Do(wl.Main.onFirstClientConnect) wl.Main.firstClientConnectOnce.Do(wl.Main.onFirstClientConnect)
@ -194,21 +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) { func (wl *WALogin) SubmitUserInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) {
ctx, cancel := context.WithTimeout(ctx, LoginConnectWait)
defer cancel()
err := wl.Client.Connect() err := wl.Client.Connect()
if err != nil { if err != nil {
wl.Log.Err(err).Msg("Failed to connect to WhatsApp for phone code login") wl.Log.Err(err).Msg("Failed to connect to WhatsApp for phone code login")
return nil, err return nil, err
} }
connectCtx, cancelConnect := context.WithTimeout(ctx, LoginConnectWait) err = wl.WaitForQRs.Wait(ctx)
err = wl.WaitForQRs.Wait(connectCtx)
cancelConnect()
if err != nil { if err != nil {
wl.Log.Warn().Err(err).Msg("Timed out waiting for connection") wl.Log.Warn().Err(err).Msg("Timed out waiting for connection")
return nil, fmt.Errorf("failed to wait for connection: %w", err) return nil, fmt.Errorf("failed to wait for connection: %w", err)
} }
pairCtx, cancelPair := context.WithTimeout(ctx, LoginPairPhoneWait) pairingCode, err := wl.Client.PairPhone(ctx, input["phone_number"], true, whatsmeow.PairClientChrome, "Chrome (Linux)")
defer cancelPair()
pairingCode, err := wl.Client.PairPhone(pairCtx, input["phone_number"], true, whatsmeow.PairClientChrome, "Chrome (Linux)")
if err != nil { if err != nil {
wl.Log.Err(err).Msg("Failed to request phone code login") wl.Log.Err(err).Msg("Failed to request phone code login")
if errors.Is(err, whatsmeow.ErrPhoneNumberTooShort) { if errors.Is(err, whatsmeow.ErrPhoneNumberTooShort) {
@ -276,17 +262,6 @@ func (wl *WALogin) handleEvent(rawEvt any) {
case *events.ClientOutdated: case *events.ClientOutdated:
wl.Log.Error().Msg("Got client outdated error") wl.Log.Error().Msg("Got client outdated error")
wl.LoginError = ErrLoginClientOutdated 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: case *events.PairSuccess:
wl.Log.Info().Any("event_data", evt).Msg("Got pair successful event") wl.Log.Info().Any("event_data", evt).Msg("Got pair successful event")
wl.LoginSuccess = evt wl.LoginSuccess = evt
@ -314,14 +289,10 @@ func (wl *WALogin) handleEvent(rawEvt any) {
func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) { func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
if wl.PhoneCode { if wl.PhoneCode {
select { err := wl.LoginComplete.Wait(ctx)
case <-ctx.Done(): if err != nil {
wl.Cancel() wl.Cancel()
return nil, ctx.Err() return nil, err
case <-wl.PasskeyRequest.GetChan():
return wl.makePasskeyStep()
case <-wl.LoginComplete.GetChan():
// continue
} }
} else { } else {
prevIndex := int(wl.PrevQRIndex.Load()) prevIndex := int(wl.PrevQRIndex.Load())
@ -348,16 +319,9 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
case <-ctx.Done(): case <-ctx.Done():
wl.Cancel() wl.Cancel()
return nil, ctx.Err() return nil, ctx.Err()
case <-wl.PasskeyRequest.GetChan():
return wl.makePasskeyStep()
case <-wl.LoginComplete.GetChan(): case <-wl.LoginComplete.GetChan():
// continue
} }
} }
return wl.onLoginComplete(ctx)
}
func (wl *WALogin) onLoginComplete(ctx context.Context) (*bridgev2.LoginStep, error) {
if wl.LoginError != nil { if wl.LoginError != nil {
wl.Log.Debug().Err(wl.LoginError).Msg("Login completed with error") wl.Log.Debug().Err(wl.LoginError).Msg("Login completed with error")
wl.Cancel() wl.Cancel()
@ -408,66 +372,6 @@ func (wl *WALogin) onLoginComplete(ctx context.Context) (*bridgev2.LoginStep, er
}, nil }, 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() { func (wl *WALogin) Cancel() {
wl.Closed.Store(true) wl.Closed.Store(true)
wl.Client.RemoveEventHandler(wl.EventHandlerID) wl.Client.RemoveEventHandler(wl.EventHandlerID)

View file

@ -21,7 +21,6 @@ import (
"encoding/json" "encoding/json"
"time" "time"
"go.mau.fi/util/ptr"
"go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow"
waBinary "go.mau.fi/whatsmeow/binary" waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types"
@ -40,16 +39,14 @@ func (wa *WhatsAppClient) initMC() {
} }
type mClient = interface { type mClient = interface {
OnMatrixEvent(any, time.Duration, error) OnConnect(version uint32, platform string)
OnWhatsAppEvent(any)
} }
type noopMC struct{} type noopMC struct{}
var noopMCInstance mClient = &noopMC{} var noopMCInstance mClient = &noopMC{}
func (n *noopMC) OnMatrixEvent(any, time.Duration, error) {} func (n *noopMC) OnConnect(version uint32, platform string) {}
func (n *noopMC) OnWhatsAppEvent(any) {}
type mWAClient = interface { type mWAClient = interface {
MSend(data []byte) 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") 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))
}

View file

@ -70,9 +70,10 @@ func (wa *WhatsAppClient) processFailedMedia(ctx context.Context, portalKey netw
func (wa *WhatsAppClient) mediaRequestLoop(ctx context.Context) { func (wa *WhatsAppClient) mediaRequestLoop(ctx context.Context) {
log := wa.UserLogin.Log.With().Str("loop", "media requests").Logger() log := wa.UserLogin.Log.With().Str("loop", "media requests").Logger()
ctx = log.WithContext(ctx) 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 var startIn time.Duration
if err == nil && userTz != nil { if tzName != "" && err == nil && userTz != nil {
now := time.Now() now := time.Now()
startAt := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, userTz) 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) 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{ return wa.Client.SendMediaRetryReceipt(ctx, &types.MessageInfo{
ID: msgID.ID, ID: msgID.ID,
MessageSource: types.MessageSource{ MessageSource: types.MessageSource{
IsFromMe: wa.IsOwnJID(msgID.Sender), IsFromMe: msgID.Sender.User == wa.JID.User,
IsGroup: msgID.Chat.Server != types.DefaultUserServer && msgID.Chat.Server != types.HiddenUserServer && msgID.Chat.Server != types.BotServer, IsGroup: msgID.Chat.Server != types.DefaultUserServer && msgID.Chat.Server != types.BotServer,
Sender: msgID.Sender, Sender: msgID.Sender,
Chat: msgID.Chat, Chat: msgID.Chat,
}, },

View file

@ -37,13 +37,6 @@ func (wa *WhatsAppClient) FillBridgeState(state status.BridgeState) status.Bridg
state.Error = WAPhoneOffline state.Error = WAPhoneOffline
state.UserAction = status.UserActionOpenNative 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 return state
} }

View file

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

View file

@ -29,8 +29,7 @@ var ResyncLoopInterval = 4 * time.Hour
var ResyncJitterSeconds = 3600 var ResyncJitterSeconds = 3600
func (wa *WhatsAppClient) EnqueueGhostResync(ghost *bridgev2.Ghost) { func (wa *WhatsAppClient) EnqueueGhostResync(ghost *bridgev2.Ghost) {
lastSync := ghost.Metadata.(*waid.GhostMetadata).LastSync.Time if ghost.Metadata.(*waid.GhostMetadata).LastSync.Add(ResyncMinInterval).After(time.Now()) {
if lastSync.Add(ResyncMinInterval).After(time.Now()) {
return return
} }
wa.resyncQueueLock.Lock() wa.resyncQueueLock.Lock()
@ -44,7 +43,6 @@ func (wa *WhatsAppClient) EnqueueGhostResync(ghost *bridgev2.Ghost) {
wa.UserLogin.Log.Debug(). wa.UserLogin.Log.Debug().
Stringer("jid", jid). Stringer("jid", jid).
Str("next_resync_in", nextResyncIn). Str("next_resync_in", nextResyncIn).
Time("last_ghost_resync", lastSync).
Msg("Enqueued resync for ghost") Msg("Enqueued resync for ghost")
} }
wa.resyncQueueLock.Unlock() wa.resyncQueueLock.Unlock()
@ -52,8 +50,7 @@ func (wa *WhatsAppClient) EnqueueGhostResync(ghost *bridgev2.Ghost) {
func (wa *WhatsAppClient) EnqueuePortalResync(portal *bridgev2.Portal, allowDM bool) { func (wa *WhatsAppClient) EnqueuePortalResync(portal *bridgev2.Portal, allowDM bool) {
jid, _ := waid.ParsePortalID(portal.ID) jid, _ := waid.ParsePortalID(portal.ID)
lastSync := portal.Metadata.(*waid.PortalMetadata).LastSync.Time if portal.Metadata.(*waid.PortalMetadata).LastSync.Add(ResyncMinInterval).After(time.Now()) {
if lastSync.Add(ResyncMinInterval).After(time.Now()) {
return return
} else if !allowDM && jid.Server != types.GroupServer { } else if !allowDM && jid.Server != types.GroupServer {
return return
@ -64,7 +61,6 @@ func (wa *WhatsAppClient) EnqueuePortalResync(portal *bridgev2.Portal, allowDM b
wa.UserLogin.Log.Debug(). wa.UserLogin.Log.Debug().
Stringer("jid", jid). Stringer("jid", jid).
Stringer("next_resync_in", time.Until(wa.nextResync)). Stringer("next_resync_in", time.Until(wa.nextResync)).
Time("last_portal_resync", lastSync).
Msg("Enqueued resync for portal") Msg("Enqueued resync for portal")
} }
wa.resyncQueueLock.Unlock() wa.resyncQueueLock.Unlock()
@ -361,7 +357,7 @@ func (wa *WhatsAppClient) fetchGhostAvatar(ctx context.Context, ghost *bridgev2.
wrappedAvatar = &bridgev2.Avatar{ wrappedAvatar = &bridgev2.Avatar{
ID: networkid.AvatarID(avatar.ID), ID: networkid.AvatarID(avatar.ID),
Get: func(ctx context.Context) ([]byte, error) { 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, "", "")
}, },
} }
} }

View file

@ -96,12 +96,12 @@ func (mq *MessageQuery) GetBetween(ctx context.Context, loginID networkid.UserLo
args := []any{mq.BridgeID, loginID, chatJID} args := []any{mq.BridgeID, loginID, chatJID}
argNum := 4 argNum := 4
if startTime != nil { if startTime != nil {
whereClauses += fmt.Sprintf(" AND timestamp > $%d", argNum) whereClauses += fmt.Sprintf(" AND timestamp >= $%d", argNum)
args = append(args, startTime.Unix()) args = append(args, startTime.Unix())
argNum++ argNum++
} }
if endTime != nil { if endTime != nil {
whereClauses += fmt.Sprintf(" AND timestamp < $%d", argNum) whereClauses += fmt.Sprintf(" AND timestamp <= $%d", argNum)
args = append(args, endTime.Unix()) args = append(args, endTime.Unix())
} }
@ -116,12 +116,9 @@ func (mq *MessageQuery) GetBetween(ctx context.Context, loginID networkid.UserLo
AsList() AsList()
} }
func (mq *MessageQuery) DeleteBetween(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID, before, after uint64) (int64, error) { func (mq *MessageQuery) DeleteBetween(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID, before, after uint64) error {
res, err := mq.Exec(ctx, deleteHistorySyncMessagesBetweenQuery, mq.BridgeID, loginID, chatJID, before, after) _, err := mq.Exec(ctx, deleteHistorySyncMessagesBetweenQuery, mq.BridgeID, loginID, chatJID, before, after)
if err != nil { return err
return 0, err
}
return res.RowsAffected()
} }
func (mq *MessageQuery) DeleteAll(ctx context.Context, loginID networkid.UserLoginID) error { 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 return err
} }
func (mq *MessageQuery) DeleteAllInChat(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID) (int64, error) { func (mq *MessageQuery) DeleteAllInChat(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID) error {
res, err := mq.Exec(ctx, deleteHistorySyncMessagesForPortalQuery, mq.BridgeID, loginID, chatJID) _, err := mq.Exec(ctx, deleteHistorySyncMessagesForPortalQuery, mq.BridgeID, loginID, chatJID)
if err != nil { return err
return 0, err
}
return res.RowsAffected()
} }
func (mq *MessageQuery) ConversationHasMessages(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID) (exists bool, err error) { func (mq *MessageQuery) ConversationHasMessages(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID) (exists bool, err error) {

View file

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

View file

@ -0,0 +1,2 @@
-- v8 (compatible with v3+): Mark LID DMs for deletion
INSERT INTO kv_store (bridge_id, key, value) VALUES ('', 'whatsapp_lid_dms_deleted', 'false');

View file

@ -0,0 +1,3 @@
-- v9 (compatible with v3+): Mark LID DMs for deletion (again)
DELETE FROM kv_store WHERE bridge_id='' AND key='whatsapp_lid_dms_deleted';
INSERT INTO kv_store (bridge_id, key, value) VALUES ('', 'whatsapp_lid_dms_deleted', 'false');

View file

@ -1,22 +0,0 @@
-- v10 (compatible with v3+): Move history sync conversations to LIDs
-- Delete history sync conversations where a @lid conversation already exists
DELETE FROM whatsapp_history_sync_conversation
WHERE chat_jid LIKE '%@lid' AND EXISTS (
SELECT 1
FROM whatsapp_history_sync_conversation pnconv
WHERE pnconv.chat_jid=(
SELECT pn || '@s.whatsapp.net'
FROM whatsmeow_lid_map
WHERE lid=replace(whatsapp_history_sync_conversation.chat_jid, '@lid', '')
)
);
-- Update all phone number conversations to lids if the lid is known
UPDATE whatsapp_history_sync_conversation
SET chat_jid=(SELECT lid || '@lid' FROM whatsmeow_lid_map WHERE pn=replace(chat_jid, '@s.whatsapp.net', ''))
WHERE chat_jid LIKE '%@s.whatsapp.net'
AND EXISTS (SELECT 1 FROM whatsmeow_lid_map WHERE pn=replace(chat_jid, '@s.whatsapp.net', ''));
-- Delete blank phone number portals
DELETE FROM portal WHERE id LIKE '%@s.whatsapp.net' AND (mxid IS NULL OR mxid='') AND room_type='';

View file

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

View file

@ -67,11 +67,10 @@ func getMessageType(waMsg *waE2E.Message) string {
return "encrypted comment" return "encrypted comment"
case waMsg.CommentMessage != nil: case waMsg.CommentMessage != nil:
return "comment" return "comment"
case waMsg.PollCreationMessage != nil || waMsg.PollCreationMessageV2 != nil || waMsg.PollCreationMessageV3 != nil || case waMsg.PollCreationMessage != nil || waMsg.PollCreationMessageV2 != nil || waMsg.PollCreationMessageV3 != nil:
waMsg.PollCreationMessageV5 != nil || waMsg.PollCreationMessageV6 != nil:
return "poll create" return "poll create"
case waMsg.PollCreationMessageV4 != nil: case waMsg.PollCreationMessageV4 != nil || waMsg.PollCreationMessageV5 != nil:
return "poll create (v4)" return "poll create (vNext)"
case waMsg.PollUpdateMessage != nil: case waMsg.PollUpdateMessage != nil:
return "poll update" return "poll update"
case waMsg.ProtocolMessage != nil: case waMsg.ProtocolMessage != nil:
@ -131,8 +130,8 @@ func getMessageType(waMsg *waE2E.Message) string {
return "secret encrypted" return "secret encrypted"
case waMsg.PollResultSnapshotMessage != nil: case waMsg.PollResultSnapshotMessage != nil:
return "poll result snapshot" return "poll result snapshot"
case waMsg.MessageHistoryNotice != nil: case waMsg.MessageHistoryBundle != nil:
return "message history notice" return "message history bundle"
case waMsg.RequestPhoneNumberMessage != nil: case waMsg.RequestPhoneNumberMessage != nil:
return "request phone number" return "request phone number"
case waMsg.KeepInChatMessage != nil: case waMsg.KeepInChatMessage != nil:
@ -153,7 +152,7 @@ func getMessageType(waMsg *waE2E.Message) string {
return "chat" return "chat"
case waMsg.PlaceholderMessage != nil: case waMsg.PlaceholderMessage != nil:
return "placeholder" return "placeholder"
case waMsg.SenderKeyDistributionMessage != nil, waMsg.StickerSyncRmrMessage != nil, waMsg.MessageHistoryBundle != nil: case waMsg.SenderKeyDistributionMessage != nil, waMsg.StickerSyncRmrMessage != nil:
return "ignore" return "ignore"
default: default:
return "unknown" return "unknown"

View file

@ -19,7 +19,6 @@ package msgconv
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -50,13 +49,7 @@ import (
"go.mau.fi/mautrix-whatsapp/pkg/waid" "go.mau.fi/mautrix-whatsapp/pkg/waid"
) )
func (mc *MessageConverter) generateContextInfo( func (mc *MessageConverter) generateContextInfo(ctx context.Context, replyTo *database.Message, portal *bridgev2.Portal, perMessageTimer *event.BeeperDisappearingTimer) *waE2E.ContextInfo {
ctx context.Context,
replyTo *database.Message,
portal *bridgev2.Portal,
perMessageTimer *event.BeeperDisappearingTimer,
roomMention bool,
) *waE2E.ContextInfo {
contextInfo := &waE2E.ContextInfo{} contextInfo := &waE2E.ContextInfo{}
if replyTo != nil { if replyTo != nil {
msgID, err := waid.ParseMessageID(replyTo.ID) msgID, err := waid.ParseMessageID(replyTo.ID)
@ -64,7 +57,6 @@ func (mc *MessageConverter) generateContextInfo(
contextInfo.StanzaID = proto.String(msgID.ID) contextInfo.StanzaID = proto.String(msgID.ID)
contextInfo.Participant = proto.String(msgID.Sender.String()) contextInfo.Participant = proto.String(msgID.Sender.String())
contextInfo.QuotedMessage = &waE2E.Message{Conversation: proto.String("")} contextInfo.QuotedMessage = &waE2E.Message{Conversation: proto.String("")}
contextInfo.QuotedType = waE2E.ContextInfo_EXPLICIT.Enum()
} else { } else {
zerolog.Ctx(ctx).Warn().Err(err). zerolog.Ctx(ctx).Warn().Err(err).
Stringer("reply_to_event_id", replyTo.MXID). Stringer("reply_to_event_id", replyTo.MXID).
@ -85,9 +77,6 @@ func (mc *MessageConverter) generateContextInfo(
if setAt > 0 && contextInfo.Expiration != nil { if setAt > 0 && contextInfo.Expiration != nil {
contextInfo.EphemeralSettingTimestamp = ptr.Ptr(setAt) contextInfo.EphemeralSettingTimestamp = ptr.Ptr(setAt)
} }
if roomMention {
contextInfo.NonJIDMentions = proto.Uint32(1)
}
return contextInfo return contextInfo
} }
@ -107,7 +96,7 @@ func (mc *MessageConverter) ToWhatsApp(
} }
message := &waE2E.Message{} 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 { switch content.MsgType {
case event.MsgText, event.MsgNotice, event.MsgEmote: case event.MsgText, event.MsgNotice, event.MsgEmote:
@ -143,14 +132,14 @@ func (mc *MessageConverter) ToWhatsApp(
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("failed to parse message ID: %w", err) return nil, nil, fmt.Errorf("failed to parse message ID: %w", err)
} }
rootMsgInfo := MessageIDToInfo(ctx, client, parsedID) rootMsgInfo := MessageIDToInfo(client, parsedID)
message, err = client.EncryptComment(ctx, rootMsgInfo, message) message, err = client.EncryptComment(ctx, rootMsgInfo, message)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("failed to encrypt comment: %w", err) return nil, nil, fmt.Errorf("failed to encrypt comment: %w", err)
} }
lid := parsedID.Sender lid := parsedID.Sender
if lid.Server == types.DefaultUserServer { if lid.Server == types.DefaultUserServer {
lid, err = client.Store.LIDs.GetLIDForPN(ctx, lid) lid, err = client.Store.LIDs.GetLIDForPN(ctx, parsedID.Sender)
if err != nil { if err != nil {
return nil, nil, fmt.Errorf("failed to get LID for PN: %w", err) return nil, nil, fmt.Errorf("failed to get LID for PN: %w", err)
} }
@ -202,7 +191,6 @@ func (mc *MessageConverter) constructMediaMessage(
FileSHA256: uploaded.FileSHA256, FileSHA256: uploaded.FileSHA256,
FileLength: proto.Uint64(uploaded.FileLength), FileLength: proto.Uint64(uploaded.FileLength),
URL: proto.String(uploaded.URL), URL: proto.String(uploaded.URL),
IsLottie: proto.Bool(mime == "application/was"),
}, },
} }
case event.MsgAudio: case event.MsgAudio:
@ -484,17 +472,6 @@ func (mc *MessageConverter) convertToWebP(img []byte) ([]byte, int, error) {
return webpBuffer.Bytes(), size, nil 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( func (mc *MessageConverter) reuploadFileToWhatsApp(
ctx context.Context, content *event.MessageEventContent, ctx context.Context, content *event.MessageEventContent,
) (*whatsmeow.UploadResponse, []byte, string, error) { ) (*whatsmeow.UploadResponse, []byte, string, error) {
@ -503,25 +480,7 @@ func (mc *MessageConverter) reuploadFileToWhatsApp(
if content.FileName != "" { if content.FileName != "" {
fileName = content.FileName fileName = content.FileName
} }
var data []byte data, err := mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
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)
}
if err != nil { if err != nil {
return nil, nil, "", fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err) return nil, nil, "", fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err)
} }
@ -539,14 +498,7 @@ func (mc *MessageConverter) reuploadFileToWhatsApp(
case event.MessageType(event.EventSticker.Type): case event.MessageType(event.EventSticker.Type):
isSticker = true isSticker = true
mediaType = whatsmeow.MediaImage mediaType = whatsmeow.MediaImage
if mime == "video/lottie+json" { if mime != "image/webp" || content.Info.Width != content.Info.Height {
// 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" {
var size int var size int
data, size, err = mc.convertToWebP(data) data, size, err = mc.convertToWebP(data)
if err != nil { if err != nil {

View file

@ -140,9 +140,6 @@ func (mc *MessageConverter) ToMatrix(
isBackfill bool, isBackfill bool,
previouslyConvertedPart *bridgev2.ConvertedMessagePart, previouslyConvertedPart *bridgev2.ConvertedMessagePart,
) *bridgev2.ConvertedMessage { ) *bridgev2.ConvertedMessage {
if waMsg == nil {
waMsg = &waE2E.Message{}
}
ctx = context.WithValue(ctx, contextKeyClient, client) ctx = context.WithValue(ctx, contextKeyClient, client)
ctx = context.WithValue(ctx, contextKeyIntent, intent) ctx = context.WithValue(ctx, contextKeyIntent, intent)
ctx = context.WithValue(ctx, contextKeyPortal, portal) ctx = context.WithValue(ctx, contextKeyPortal, portal)
@ -175,12 +172,6 @@ func (mc *MessageConverter) ToMatrix(
part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV2) part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV2)
case waMsg.PollCreationMessageV3 != nil: case waMsg.PollCreationMessageV3 != nil:
part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV3) part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV3)
//case waMsg.PollCreationMessageV4 != nil:
// part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV4)
case waMsg.PollCreationMessageV5 != nil:
part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV5)
case waMsg.PollCreationMessageV6 != nil:
part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV6)
case waMsg.PollUpdateMessage != nil: case waMsg.PollUpdateMessage != nil:
part, contextInfo = mc.convertPollUpdateMessage(ctx, info, waMsg.PollUpdateMessage) part, contextInfo = mc.convertPollUpdateMessage(ctx, info, waMsg.PollUpdateMessage)
case waMsg.EventMessage != nil: case waMsg.EventMessage != nil:
@ -221,8 +212,6 @@ func (mc *MessageConverter) ToMatrix(
part, contextInfo = mc.convertPlaceholderMessage(ctx, waMsg) part, contextInfo = mc.convertPlaceholderMessage(ctx, waMsg)
case waMsg.GroupInviteMessage != nil: case waMsg.GroupInviteMessage != nil:
part, contextInfo = mc.convertGroupInviteMessage(ctx, info, waMsg.GroupInviteMessage) 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: case waMsg.ProtocolMessage != nil && waMsg.ProtocolMessage.GetType() == waE2E.ProtocolMessage_EPHEMERAL_SETTING:
part, contextInfo = mc.convertEphemeralSettingMessage(ctx, waMsg.ProtocolMessage, info.Timestamp, isBackfill) part, contextInfo = mc.convertEphemeralSettingMessage(ctx, waMsg.ProtocolMessage, info.Timestamp, isBackfill)
case waMsg.EncCommentMessage != nil: case waMsg.EncCommentMessage != nil:
@ -245,9 +234,6 @@ func (mc *MessageConverter) ToMatrix(
part.Extra["fi.mau.whatsapp.source_broadcast_list"] = info.Chat.String() part.Extra["fi.mau.whatsapp.source_broadcast_list"] = info.Chat.String()
} }
mc.addMentions(ctx, contextInfo.GetMentionedJID(), part.Content) mc.addMentions(ctx, contextInfo.GetMentionedJID(), part.Content)
if contextInfo.GetNonJIDMentions() == 1 {
part.Content.Mentions.Room = true
}
cm := &bridgev2.ConvertedMessage{ cm := &bridgev2.ConvertedMessage{
Parts: []*bridgev2.ConvertedMessagePart{part}, Parts: []*bridgev2.ConvertedMessagePart{part},
@ -271,26 +257,30 @@ func (mc *MessageConverter) ToMatrix(
if chat.IsEmpty() { if chat.IsEmpty() {
chat, _ = waid.ParsePortalID(portal.ID) chat, _ = waid.ParsePortalID(portal.ID)
} }
// We reroute all DMs to the phone number JID, so reroute reply participants too
pcp = rerouteMessageKey(ctx, chat, pcp, getPortal(ctx).Metadata.(*waid.PortalMetadata).AddressingMode == types.AddressingModeLID)
if store := getClient(ctx).Store; store != nil && chat.Server == types.DefaultUserServer && pcp.Server == types.HiddenUserServer {
pcpPN, _ := store.LIDs.GetPNForLID(ctx, pcp)
zerolog.Ctx(ctx).Debug().
Stringer("orig_participant", pcp).
Stringer("rerouted_participant", pcpPN).
Msg("Rerouting reply target (PN recipient in LID DM)")
if !pcpPN.IsEmpty() {
pcp = pcpPN
}
} else if store != nil && chat.Server == types.GroupServer && pcp.Server == types.DefaultUserServer && getPortal(ctx).Metadata.(*waid.PortalMetadata).AddressingMode == types.AddressingModeLID {
pcpLID, _ := store.LIDs.GetLIDForPN(ctx, pcp)
zerolog.Ctx(ctx).Debug().
Stringer("orig_participant", pcp).
Stringer("rerouted_participant", pcpLID).
Msg("Rerouting reply target (PN recipient in LID group)")
if !pcpLID.IsEmpty() {
pcp = pcpLID
}
}
cm.ReplyTo = &networkid.MessageOptionalPartID{ cm.ReplyTo = &networkid.MessageOptionalPartID{
MessageID: waid.MakeMessageID(chat, pcp, contextInfo.GetStanzaID()), MessageID: waid.MakeMessageID(chat, pcp, contextInfo.GetStanzaID()),
} }
var pn, lid types.JID
if pcp.Server == types.DefaultUserServer {
pn = pcp
lid, _ = client.Store.LIDs.GetLIDForPN(ctx, pcp)
} else if pcp.Server == types.HiddenUserServer {
lid = pcp
pn, _ = client.Store.LIDs.GetPNForLID(ctx, pcp)
} else if pcp.Server == types.BotServer {
lid = pcp
}
if !pn.IsEmpty() {
cm.ReplyToLogin = waid.MakeUserLoginID(pn)
}
if !lid.IsEmpty() {
cm.ReplyToUser = waid.MakeUserID(lid)
}
// TODO set reply to room
} }
if contextInfo.GetIsForwarded() { if contextInfo.GetIsForwarded() {
hasCaption := part.Content.FileName != "" && part.Content.FileName != part.Content.Body hasCaption := part.Content.FileName != "" && part.Content.FileName != part.Content.Body

View file

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

View file

@ -17,9 +17,6 @@
package msgconv package msgconv
import ( import (
"sync"
"go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/format" "maunium.net/go/mautrix/format"
@ -46,16 +43,12 @@ type MessageConverter struct {
DisableViewOnce bool DisableViewOnce bool
DirectMedia bool DirectMedia bool
OldMediaSuffix string OldMediaSuffix string
stickerPackCache map[string]*types.StickerPack
stickerPackCacheLock sync.Mutex
} }
func New(br *bridgev2.Bridge) *MessageConverter { func New(br *bridgev2.Bridge) *MessageConverter {
mc := &MessageConverter{ mc := &MessageConverter{
Bridge: br, Bridge: br,
MaxFileSize: 50 * 1024 * 1024, MaxFileSize: 50 * 1024 * 1024,
stickerPackCache: make(map[string]*types.StickerPack),
} }
mc.HTMLParser = &format.HTMLParser{ mc.HTMLParser = &format.HTMLParser{
PillConverter: mc.convertPill, PillConverter: mc.convertPill,

View file

@ -66,10 +66,10 @@ func (mc *MessageConverter) convertTemplateMessage(ctx context.Context, info *ty
if addButtonText { if addButtonText {
description += "\nUse the WhatsApp app to click buttons" 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 != "" { 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 var convertedTitle *bridgev2.ConvertedMessagePart
@ -239,7 +239,7 @@ func (mc *MessageConverter) postProcessBusinessMessage(content string, headerMed
converted.Content.Body += content converted.Content.Body += content
contentHTML := parseWAFormattingToHTML(content, true) contentHTML := parseWAFormattingToHTML(content, true)
if contentHTML != event.TextToHTML(content) || converted.Content.FormattedBody != "" { if contentHTML != event.TextToHTML(content) || converted.Content.FormattedBody != "" {
converted.Content.Format = event.FormatHTML converted.Content.EnsureHasHTML()
if converted.Content.FormattedBody != "" { if converted.Content.FormattedBody != "" {
converted.Content.FormattedBody += "<br><br>" converted.Content.FormattedBody += "<br><br>"
} }

View file

@ -20,7 +20,6 @@ import (
"bytes" "bytes"
"context" "context"
"fmt" "fmt"
"html"
"image" "image"
"math" "math"
"net/http" "net/http"
@ -52,7 +51,7 @@ func (mc *MessageConverter) convertLocationMessage(ctx context.Context, msg *waE
MsgType: event.MsgLocation, MsgType: event.MsgLocation,
Body: fmt.Sprintf("Location: %s\n%s\n%s", name, msg.GetAddress(), url), Body: fmt.Sprintf("Location: %s\n%s\n%s", name, msg.GetAddress(), url),
Format: event.FormatHTML, Format: event.FormatHTML,
FormattedBody: fmt.Sprintf(`Location: <a href="%s">%s</a><br>%s`, html.EscapeString(url), html.EscapeString(name), html.EscapeString(msg.GetAddress())), FormattedBody: fmt.Sprintf("Location: <a href='%s'>%s</a><br>%s", url, name, msg.GetAddress()),
GeoURI: fmt.Sprintf("geo:%.5f,%.5f", msg.GetDegreesLatitude(), msg.GetDegreesLongitude()), GeoURI: fmt.Sprintf("geo:%.5f,%.5f", msg.GetDegreesLatitude(), msg.GetDegreesLongitude()),
} }

View file

@ -17,6 +17,8 @@
package msgconv package msgconv
import ( import (
"archive/zip"
"bytes"
"context" "context"
"encoding/json" "encoding/json"
"errors" "errors"
@ -24,19 +26,21 @@ import (
"io" "io"
"net/http" "net/http"
"os" "os"
"path/filepath"
"strconv"
"strings" "strings"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.mau.fi/util/exfmt"
"go.mau.fi/util/exmime" "go.mau.fi/util/exmime"
"go.mau.fi/util/exslices" "go.mau.fi/util/exslices"
"go.mau.fi/util/lottie"
"go.mau.fi/util/random"
"go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/proto/waE2E" "go.mau.fi/whatsmeow/proto/waE2E"
"go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database" "maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-whatsapp/pkg/waid" "go.mau.fi/mautrix-whatsapp/pkg/waid"
) )
@ -83,15 +87,15 @@ func (mc *MessageConverter) convertMediaMessage(
MimeType: msg.GetMimetype(), MimeType: msg.GetMimetype(),
} }
if mc.DirectMedia { if mc.DirectMedia {
preparedMedia.FillFileName()
if preparedMedia.Info.MimeType == "application/was" { if preparedMedia.Info.MimeType == "application/was" {
preparedMedia.Info.MimeType = "video/lottie+json" preparedMedia.Info.MimeType = "video/lottie+json"
preparedMedia.FileName = "sticker.json" preparedMedia.FileName = "sticker.json"
} }
preparedMedia.FillFileName()
var err error var err error
portal := getPortal(ctx) portal := getPortal(ctx)
idOverride := getEditTargetID(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 { if err != nil {
panic(fmt.Errorf("failed to generate content URI: %w", err)) panic(fmt.Errorf("failed to generate content URI: %w", err))
} }
@ -120,20 +124,13 @@ func (mc *MessageConverter) convertMediaMessage(
return 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) { func (mc *MessageConverter) convertAlbumMessage(ctx context.Context, msg *waE2E.AlbumMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
parts := make([]string, 0, 2) parts := make([]string, 0, 2)
if msg.GetExpectedImageCount() > 0 { 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 { 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 var partDesc string
if len(parts) > 0 { if len(parts) > 0 {
@ -201,9 +198,7 @@ type PreparedMedia struct {
} }
func (pm *PreparedMedia) FillFileName() *PreparedMedia { func (pm *PreparedMedia) FillFileName() *PreparedMedia {
if pm.Type == event.EventSticker { if pm.FileName == "" {
pm.FileName = ""
} else if pm.FileName == "" {
pm.FileName = strings.TrimPrefix(string(pm.MsgType), "m.") + exmime.ExtensionFromMimetype(pm.Info.MimeType) pm.FileName = strings.TrimPrefix(string(pm.MsgType), "m.") + exmime.ExtensionFromMimetype(pm.Info.MimeType)
} }
return pm return pm
@ -244,19 +239,6 @@ type MediaMessageWithDuration interface {
const WhatsAppStickerSize = 190 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 { func prepareMediaMessage(rawMsg MediaMessage) *PreparedMedia {
extraInfo := map[string]any{} extraInfo := map[string]any{}
data := &PreparedMedia{ data := &PreparedMedia{
@ -305,7 +287,19 @@ func prepareMediaMessage(rawMsg MediaMessage) *PreparedMedia {
case *waE2E.StickerMessage: case *waE2E.StickerMessage:
data.Type = event.EventSticker data.Type = event.EventSticker
data.FileName = "sticker" + exmime.ExtensionFromMimetype(msg.GetMimetype()) 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: case *waE2E.VideoMessage:
data.MsgType = event.MsgVideo data.MsgType = event.MsgVideo
pairedMediaType := msg.GetContextInfo().GetPairedMediaType() pairedMediaType := msg.GetContextInfo().GetPairedMediaType()
@ -365,17 +359,14 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
) error { ) error {
client := getClient(ctx) client := getClient(ctx)
intent := getIntent(ctx) intent := getIntent(ctx)
var roomID id.RoomID portal := getPortal(ctx)
if portal := getPortal(ctx); portal != nil {
roomID = portal.MXID
}
var thumbnailData []byte var thumbnailData []byte
var thumbnailInfo *event.FileInfo var thumbnailInfo *event.FileInfo
if part.Info.Size > uploadFileThreshold { if part.Info.Size > uploadFileThreshold {
var err error 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)) 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") zerolog.Ctx(ctx).Warn().Err(err).Msg("Mismatching media checksums in message. Ignoring because WhatsApp seems to ignore them too")
} else if err != nil { } else if err != nil {
return nil, fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err) return nil, fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err)
@ -396,7 +387,7 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
} }
} else { } else {
data, err := client.Download(ctx, message) 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") zerolog.Ctx(ctx).Warn().Err(err).Msg("Mismatching media checksums in message. Ignoring because WhatsApp seems to ignore them too")
} else if err != nil { } else if err != nil {
return fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err) return fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err)
@ -406,14 +397,12 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
if err != nil { if err != nil {
return err return err
} }
} else if part.Type == event.EventSticker && part.Info.MimeType == "image/webp" {
mc.fillWebPStickerInfo(ctx, part, data)
} }
if part.Info.MimeType == "" { if part.Info.MimeType == "" {
part.Info.MimeType = http.DetectContentType(data) part.Info.MimeType = http.DetectContentType(data)
} }
part.FillFileName() 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 { if err != nil {
return fmt.Errorf("%w: %w", bridgev2.ErrMediaReuploadFailed, err) return fmt.Errorf("%w: %w", bridgev2.ErrMediaReuploadFailed, err)
} }
@ -422,7 +411,7 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
var err error var err error
part.Info.ThumbnailURL, part.Info.ThumbnailFile, err = intent.UploadMedia( part.Info.ThumbnailURL, part.Info.ThumbnailFile, err = intent.UploadMedia(
ctx, ctx,
roomID, portal.MXID,
thumbnailData, thumbnailData,
"thumbnail"+exmime.ExtensionFromMimetype(thumbnailInfo.MimeType), "thumbnail"+exmime.ExtensionFromMimetype(thumbnailInfo.MimeType),
thumbnailInfo.MimeType, thumbnailInfo.MimeType,
@ -436,6 +425,68 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
return nil 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 { func (mc *MessageConverter) makeMediaFailure(ctx context.Context, mediaInfo *PreparedMedia, keys *FailedMediaKeys, err error) *bridgev2.ConvertedMessagePart {
logLevel := zerolog.ErrorLevel logLevel := zerolog.ErrorLevel
var extra map[string]any var extra map[string]any
@ -480,3 +531,28 @@ func (mc *MessageConverter) makeMediaFailure(ctx context.Context, mediaInfo *Pre
} }
return part 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
}

View file

@ -20,16 +20,14 @@ import (
"context" "context"
"encoding/base64" "encoding/base64"
"fmt" "fmt"
"html"
"html/template" "html/template"
"strings" "strings"
"time" "time"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.mau.fi/util/exerrors" "go.mau.fi/util/exerrors"
"go.mau.fi/util/exfmt"
"go.mau.fi/util/ptr" "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/proto/waE2E"
"go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types"
"google.golang.org/protobuf/proto" "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{ content := &event.MessageEventContent{
MsgType: event.MsgText, MsgType: event.MsgText,
Body: format.HTMLToText(htmlMessage), Body: format.HTMLToText(htmlMessage),
@ -119,92 +117,6 @@ func (mc *MessageConverter) convertGroupInviteMessage(ctx context.Context, info
}, msg.GetContextInfo() }, 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) { func (mc *MessageConverter) convertEphemeralSettingMessage(ctx context.Context, msg *waE2E.ProtocolMessage, ts time.Time, isBackfill bool) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
portal := getPortal(ctx) portal := getPortal(ctx)
portalMeta := portal.Metadata.(*waid.PortalMetadata) 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) { func (mc *MessageConverter) convertRichResponseMessage(ctx context.Context, msg *waE2E.AIRichResponseMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
var body strings.Builder var body strings.Builder
// TODO switch to new format?
for i, submsg := range msg.GetSubmessages() { 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 { if i > 0 {
body.WriteString("\n") body.WriteString("\n")
} }

View file

@ -94,7 +94,31 @@ func (mc *MessageConverter) convertPollCreationMessage(ctx context.Context, msg
}, msg.GetContextInfo() }, msg.GetContextInfo()
} }
func rerouteMessageKey(ctx context.Context, chat, sender types.JID, groupLIDAddressing bool) types.JID {
if store := getClient(ctx).Store; store != nil && chat.Server == types.DefaultUserServer && sender.Server == types.HiddenUserServer {
senderPN, _ := store.LIDs.GetPNForLID(ctx, sender)
zerolog.Ctx(ctx).Debug().
Stringer("orig_participant", sender).
Stringer("rerouted_participant", senderPN).
Msg("Rerouting message key (PN recipient in LID DM)")
if !senderPN.IsEmpty() {
return senderPN
}
} else if store != nil && chat.Server == types.GroupServer && sender.Server == types.DefaultUserServer && groupLIDAddressing {
senderLID, _ := store.LIDs.GetLIDForPN(ctx, sender)
zerolog.Ctx(ctx).Debug().
Stringer("orig_participant", sender).
Stringer("rerouted_participant", senderLID).
Msg("Rerouting message key (PN recipient in LID group)")
if !senderLID.IsEmpty() {
return senderLID
}
}
return sender
}
func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender types.JID, key *waCommon.MessageKey) networkid.MessageID { func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender types.JID, key *waCommon.MessageKey) networkid.MessageID {
groupLIDAddressing := sender.Server == types.HiddenUserServer
sender = sender.ToNonAD() sender = sender.ToNonAD()
var err error var err error
if !key.GetFromMe() { if !key.GetFromMe() {
@ -107,7 +131,7 @@ func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender
if sender.Server == types.LegacyUserServer { if sender.Server == types.LegacyUserServer {
sender.Server = types.DefaultUserServer sender.Server = types.DefaultUserServer
} }
} else if chat.Server == types.DefaultUserServer || chat.Server == types.HiddenUserServer || chat.Server == types.BotServer { } else if chat.Server == types.DefaultUserServer || chat.Server == types.BotServer {
if sender.User == client.Store.GetJID().User || sender.User == client.Store.GetLID().User { if sender.User == client.Store.GetJID().User || sender.User == client.Store.GetLID().User {
// Message key is not from the sender, but message sender (containing key) is me, // Message key is not from the sender, but message sender (containing key) is me,
// so message key sender is the other user in the DM // so message key sender is the other user in the DM
@ -115,11 +139,7 @@ func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender
} else { } else {
// Message key is not from the sender, but message sender (containing key) is not me, // Message key is not from the sender, but message sender (containing key) is not me,
// so message key sender is me // so message key sender is me
if chat.Server == types.HiddenUserServer { sender = client.Store.GetJID().ToNonAD()
sender = client.Store.GetLID().ToNonAD()
} else {
sender = client.Store.GetJID().ToNonAD()
}
} }
} else { } else {
zerolog.Ctx(ctx).Warn(). zerolog.Ctx(ctx).Warn().
@ -137,6 +157,10 @@ func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender
chat = remoteJID chat = remoteJID
} }
} }
sender = rerouteMessageKey(
context.WithValue(ctx, contextKeyClient, client),
chat, sender, groupLIDAddressing,
)
return waid.MakeMessageID(chat, sender, key.GetID()) return waid.MakeMessageID(chat, sender, key.GetID())
} }
@ -153,9 +177,6 @@ func (mc *MessageConverter) convertPollUpdateMessage(ctx context.Context, info *
if err != nil { if err != nil {
log.Err(err).Msg("Failed to get poll update target message") log.Err(err).Msg("Failed to get poll update target message")
return failedPollUpdatePart, nil 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{ vote, err := getClient(ctx).DecryptPollVote(ctx, &events.Message{
Info: *info, Info: *info,

View file

@ -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 <https://www.gnu.org/licenses/>.
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)
}

View file

@ -41,8 +41,7 @@ type UserLoginMetadata struct {
AppStateRecoveryAttempted map[appstate.WAPatchName]time.Time `json:"app_state_recovery_attempted,omitempty"` AppStateRecoveryAttempted map[appstate.WAPatchName]time.Time `json:"app_state_recovery_attempted,omitempty"`
HistorySyncPortalsNeedCreating bool `json:"history_sync_portals_need_creating,omitempty"` HistorySyncPortalsNeedCreating bool `json:"history_sync_portals_need_creating,omitempty"`
ReachoutTimelockUntil time.Time `json:"reachout_timelock_until,omitempty"`
MData json.RawMessage `json:"mdata,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 type MessageErrorType string
const ( const (

View file

@ -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)) return networkid.MessageID(fmt.Sprintf("%s:%s:%s", chat.ToNonAD().String(), sender.ToNonAD().String(), id))
} }
func MakeMessageIDWithAltSender(chat, sender, altSender types.JID, id types.MessageID) networkid.MessageID {
if chat.Server == types.HiddenUserServer && sender.Server == types.DefaultUserServer && altSender.Server == types.HiddenUserServer {
sender = altSender
}
return MakeMessageID(chat, sender, id)
}
func MakeFakeMessageID(chat, sender types.JID, data string) networkid.MessageID { func MakeFakeMessageID(chat, sender types.JID, data string) networkid.MessageID {
return networkid.MessageID(fmt.Sprintf("fake:%s:%s:%s", chat.ToNonAD().String(), sender.ToNonAD().String(), data)) return networkid.MessageID(fmt.Sprintf("fake:%s:%s:%s", chat.ToNonAD().String(), sender.ToNonAD().String(), data))
} }
type ParsedMessageID struct { type ParsedMessageID struct {
Chat types.JID Chat types.JID
Sender types.JID Sender types.JID
ID types.MessageID ID types.MessageID
Version []byte
} }
func (pmi *ParsedMessageID) String() networkid.MessageID { func (pmi *ParsedMessageID) String() networkid.MessageID {

View file

@ -33,10 +33,9 @@ const (
mediaIDTypeMessage = 255 mediaIDTypeMessage = 255
mediaIDTypeAvatar = 254 mediaIDTypeAvatar = 254
mediaIDTypeCommunityAvatar = 253 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()) compactChat := compactJID(messageInfo.Chat.ToNonAD())
compactSender := compactJID(messageInfo.Sender.ToNonAD()) compactSender := compactJID(messageInfo.Sender.ToNonAD())
receiverID := compactJID(ParseUserLoginID(receiver, 0)) receiverID := compactJID(ParseUserLoginID(receiver, 0))
@ -46,7 +45,7 @@ func MakeMediaID(messageInfo *types.MessageInfo, idOverride types.MessageID, rec
} else { } else {
compactID = compactMsgID(messageInfo.ID) 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, mediaIDTypeMessage)
mediaID = append(mediaID, byte(len(compactChat))) mediaID = append(mediaID, byte(len(compactChat)))
mediaID = append(mediaID, 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, receiverID...)
mediaID = append(mediaID, byte(len(compactID))) mediaID = append(mediaID, byte(len(compactID)))
mediaID = append(mediaID, compactID...) mediaID = append(mediaID, compactID...)
mediaID = append(mediaID, byte(len(version)))
mediaID = append(mediaID, version...)
return mediaID return mediaID
} }
@ -85,28 +82,9 @@ type AvatarMediaInfo struct {
Community bool 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 { type ParsedMediaID struct {
Message *ParsedMessageID Message *ParsedMessageID
Avatar *AvatarMediaInfo Avatar *AvatarMediaInfo
Sticker *StickerPackMediaInfo
UserLogin networkid.UserLoginID UserLogin networkid.UserLoginID
} }
@ -140,12 +118,6 @@ func ParseMediaID(mediaID networkid.MediaID) (*ParsedMediaID, error) {
Sender: senderJID, Sender: senderJID,
ID: id, 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) parsed.UserLogin = MakeUserLoginID(receiverID)
case mediaIDTypeAvatar, mediaIDTypeCommunityAvatar: case mediaIDTypeAvatar, mediaIDTypeCommunityAvatar:
targetJID, err := readCompact(&mediaID, parseCompactJID) targetJID, err := readCompact(&mediaID, parseCompactJID)
@ -166,24 +138,6 @@ func ParseMediaID(mediaID networkid.MediaID) (*ParsedMediaID, error) {
Community: mediaIDType == mediaIDTypeCommunityAvatar, Community: mediaIDType == mediaIDTypeCommunityAvatar,
} }
parsed.UserLogin = MakeUserLoginID(receiverID) 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: default:
return nil, fmt.Errorf("unknown media ID type %d", mediaIDType) 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) { func readCompact[T any](data *networkid.MediaID, fn func(data []byte) (T, error)) (T, error) {
var defVal T var defVal T
if len(*data) < 1 { if len(*data) < 1 {