1
0
Fork 0
mirror of https://github.com/mautrix/whatsapp.git synced 2026-07-06 14:23:20 -04:00

Compare commits

..

1 commit

Author SHA1 Message Date
Nick Mills-Barrett
a2f5fc0f51
handlewhatsapp: fix unintended drop when handling undecryptable message 2026-02-26 19:07:19 +00:00
37 changed files with 585 additions and 1480 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
<!-- All items below are mandatory. Issues not following the rules may be closed without comment. -->
* [ ] This is an actual bug, not just a setup issue (see the [troubleshooting docs](https://docs.mau.fi/bridges/general/troubleshooting.html) or ask in the Matrix room for setup help).
* [ ] I am certain that sufficient information is included. Ask in the Matrix room first if not.
* [ ] The bug is still present on the main branch. The `!wa version` command output is: ``
It's always best to ask in the Matrix room first, especially if you aren't sure
what details are needed. Issues with insufficient detail will likely just be
ignored or closed immediately.
-->

View file

@ -1,30 +1,3 @@
# v26.06
* Added placeholder for group message history share notices.
* Fixed community spaces not being bridged properly in some cases.
* Fixed handling edits of HD media.
* Fixed duplicate message when a message from another own device is initially
undecryptable and later gets resolved.
# v26.05
* Added support for importing sticker packs from WhatsApp.
* Added support for WhatsApp's new message edit encryption scheme.
# v26.04
* Added support for @room mentions in both directions.
* Changed initial backfill to happen even if WhatsApp doesn't send full history.
* Fixed panic when handling updates to unknown polls from WhatsApp.
* Fixed some background loops not stopping when a user is logged out.
# v26.03
* Added option to save outgoing messages in the database to allow encryption
retries to work across restarts.
* Fixed contact list API not returning some contacts.
* Fixed business template messages with media duplicating the text part.
# v26.02
* Bumped minimum Go version to 1.25.

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",
URL: "https://github.com/mautrix/whatsapp",
Description: "A Matrix-WhatsApp puppeting bridge.",
Version: "26.06",
Version: "26.02",
SemCalVer: true,
Connector: &connector.WhatsAppConnector{},
}
func main() {
m.PostStart = func() {
if m.Matrix.Provisioning != nil {
m.Matrix.Provisioning.Router.HandleFunc("GET /v1/contacts", legacyProvContacts)
m.Matrix.Provisioning.Router.HandleFunc("GET /v1/resolve_identifier/{number}", legacyProvResolveIdentifier)
m.Matrix.Provisioning.Router.HandleFunc("POST /v1/pm/{number}", legacyProvResolveIdentifier)
m.Matrix.Provisioning.Router.HandleFunc("POST /v1/debug/appstate/{patch}", provAppStateDebug)
m.Matrix.Provisioning.Router.HandleFunc("POST /v1/debug/recover-appstate/{patch}", provRecoverAppStateDebug)
}
}
m.InitVersion(Tag, Commit, BuildTime)
m.Run()
}

46
go.mod
View file

@ -2,52 +2,52 @@ module go.mau.fi/mautrix-whatsapp
go 1.25.0
toolchain go1.26.4
toolchain go1.26.0
tool go.mau.fi/util/cmd/maubuild
require (
github.com/lib/pq v1.12.3
github.com/rs/zerolog v1.35.1
github.com/tidwall/gjson v1.19.0
go.mau.fi/util v0.9.10
go.mau.fi/webp v0.3.0
go.mau.fi/whatsmeow v0.0.0-20260630180629-b572e5bcb92b
golang.org/x/image v0.42.0
golang.org/x/net v0.56.0
golang.org/x/sync v0.21.0
github.com/lib/pq v1.11.2
github.com/rs/zerolog v1.34.0
go.mau.fi/util v0.9.6
go.mau.fi/webp v0.2.0
go.mau.fi/whatsmeow v0.0.0-20260218131543-e4d82a04d5d8
golang.org/x/image v0.36.0
golang.org/x/net v0.50.0
golang.org/x/sync v0.19.0
google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1
maunium.net/go/mautrix v0.28.2-0.20260702123919-bf2dbb2aa895
maunium.net/go/mautrix v0.26.3
)
require (
filippo.io/edwards25519 v1.2.0 // indirect
filippo.io/edwards25519 v1.1.0 // indirect
github.com/beeper/argo-go v1.1.2 // indirect
github.com/coder/websocket v1.8.15 // indirect
github.com/coreos/go-systemd/v22 v22.7.0 // indirect
github.com/coder/websocket v1.8.14 // indirect
github.com/coreos/go-systemd/v22 v22.6.0 // indirect
github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.45 // indirect
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect
github.com/mattn/go-sqlite3 v1.14.34 // indirect
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect
github.com/vektah/gqlparser/v2 v2.5.27 // indirect
github.com/yuin/goldmark v1.8.2 // indirect
go.mau.fi/libsignal v0.2.2 // indirect
github.com/yuin/goldmark v1.7.16 // indirect
go.mau.fi/libsignal v0.2.1 // indirect
go.mau.fi/zeroconfig v0.2.0 // indirect
golang.org/x/crypto v0.53.0 // indirect
golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect
golang.org/x/mod v0.37.0 // indirect
golang.org/x/sys v0.46.0 // indirect
golang.org/x/text v0.38.0 // indirect
golang.org/x/crypto v0.48.0 // indirect
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a // indirect
golang.org/x/mod v0.33.0 // indirect
golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.34.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
maunium.net/go/mauflag v1.0.0 // indirect

96
go.sum
View file

@ -1,5 +1,5 @@
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
@ -8,15 +8,17 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNg
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/beeper/argo-go v1.1.2 h1:UQI2G8F+NLfGTOmTUI0254pGKx/HUU/etbUGTJv91Fs=
github.com/beeper/argo-go v1.1.2/go.mod h1:M+LJAnyowKVQ6Rdj6XYGEn+qcVFkb3R/MUpqkGR0hM4=
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@ -28,17 +30,21 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.45 h1:6KA/spDguL3KV8rnybG7ezSaE4SeMR3KC9VbUoAQaIk=
github.com/mattn/go-sqlite3 v1.14.45/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM=
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741 h1:KPpdlQLZcHfTMQRi6bFQ7ogNO0ltFT4PmtwTLW4W+14=
github.com/petermattis/goid v0.0.0-20260113132338-7c7de50cc741/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
@ -46,8 +52,8 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
@ -55,8 +61,8 @@ github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDq
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
@ -67,35 +73,37 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=
github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/libsignal v0.2.2 h1:QV+XdzQkm3x3aSG7FcqfGSZuFXz83pRZPBFaPygHbOU=
go.mau.fi/libsignal v0.2.2/go.mod h1:CRlIQg2J8uYTfDFvNoO8/KcZjs5cey0vbc6oj/bssY0=
go.mau.fi/util v0.9.10 h1:wzvz5iDHyqDXB8vgisD4d3SzucLXNM3iNY+1O1RoHtg=
go.mau.fi/util v0.9.10/go.mod h1:YQOxySn+ZE3qSYqNxvyX7Yi3suA8YK17PS6QqBREW7A=
go.mau.fi/webp v0.3.0 h1:gVHQZtz21Ziwj+CDuklbX9mqpsnDIFKxs/BJyV7iZzA=
go.mau.fi/webp v0.3.0/go.mod h1:rlZFTev+dYxhvk+XNBP/5GcTt4gXmzAB4DU0aGUYIQo=
go.mau.fi/whatsmeow v0.0.0-20260630180629-b572e5bcb92b h1:ZUk1ErarDNpnbosXR/MeOz2gkqA4S1bh8zjaSRj7N+Y=
go.mau.fi/whatsmeow v0.0.0-20260630180629-b572e5bcb92b/go.mod h1:9dmNTYZ/1pHjPw/bz+azBsGjAkcrZbqzMrKcvG5bJ8U=
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/libsignal v0.2.1 h1:vRZG4EzTn70XY6Oh/pVKrQGuMHBkAWlGRC22/85m9L0=
go.mau.fi/libsignal v0.2.1/go.mod h1:iVvjrHyfQqWajOUaMEsIfo3IqgVMrhWcPiiEzk7NgoU=
go.mau.fi/util v0.9.6 h1:2nsvxm49KhI3wrFltr0+wSUBlnQ4CMtykuELjpIU+ts=
go.mau.fi/util v0.9.6/go.mod h1:sIJpRH7Iy5Ad1SBuxQoatxtIeErgzxCtjd/2hCMkYMI=
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
go.mau.fi/whatsmeow v0.0.0-20260218131543-e4d82a04d5d8 h1:riEnRpKjNnVLuaGIm+9Q3SozatMsseJLxnc3ZWH6Exo=
go.mau.fi/whatsmeow v0.0.0-20260218131543-e4d82a04d5d8/go.mod h1:mXCRFyPEPn4jqWz6Afirn8vY7DpHCPnlKq6I2cWwFHM=
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
golang.org/x/crypto v0.53.0 h1:QZ4Muo8THX6CizN2vPPd5fBGHyogrdK9fG4wLPFUsto=
golang.org/x/crypto v0.53.0/go.mod h1:DNLU434OwVakk9PzuwV8w62mAJpRJL3vsgcfp4Qnsio=
golang.org/x/exp v0.0.0-20260611194520-c48552f49976 h1:X8Hz2ImujgbmetVuW+w2YkyZChE3cBpZi2P158rTG9M=
golang.org/x/exp v0.0.0-20260611194520-c48552f49976/go.mod h1:vnf4pv9iKZXY58sQE1L86zmNWJ4159e1RkcWiLCkeEY=
golang.org/x/image v0.42.0 h1:1gSs6ehNWXLbkHBIPcWztk3D/6aIA/8hauiAYtlodVY=
golang.org/x/image v0.42.0/go.mod h1:rrpelvGFt+kLPAjPM4HeWPgrl0FtafueU//e5N0qk/Q=
golang.org/x/mod v0.37.0 h1:vF1DjpVEshcIqoEaauuHebaLk1O1forxjxBaVn884JQ=
golang.org/x/mod v0.37.0/go.mod h1:m8S8VeM9r4dzDwjrKO0a1sZP3YjeMamRRlD+fmR2Q/0=
golang.org/x/net v0.56.0 h1:Rw8j/hFzGvJUZwNBXnAtf5sVDVt+65SK2C7IxCxZt5o=
golang.org/x/net v0.56.0/go.mod h1:D3Ku6r+V6JROoZK144D2XfMHFcMq/0zSfLelVTCFKec=
golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
golang.org/x/sync v0.21.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a h1:ovFr6Z0MNmU7nH8VaX5xqw+05ST2uO1exVfZPVqRC5o=
golang.org/x/exp v0.0.0-20260212183809-81e46e3db34a/go.mod h1:K79w1Vqn7PoiZn+TkNpx3BUWUQksGO3JcVX6qIjytmA=
golang.org/x/image v0.36.0 h1:Iknbfm1afbgtwPTmHnS2gTM/6PPZfH+z2EFuOkSbqwc=
golang.org/x/image v0.36.0/go.mod h1:YsWD2TyyGKiIX1kZlu9QfKIsQ4nAAK9bdgdrIsE7xy4=
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.38.0 h1:sXmwo9DwP3OK9EZ7PqAdaooSGozfl/3a6/xJcbzPRhE=
golang.org/x/text v0.38.0/go.mod h1:YXZt3QhHUKYT53r2lLKFIVi6Ao1jdzrTR/KQ09qyxF4=
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
@ -107,5 +115,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
maunium.net/go/mautrix v0.28.2-0.20260702123919-bf2dbb2aa895 h1:oe87/mP1vo4LnA6CJDEfyg5ZiAOar2quJNypfO8eS4Y=
maunium.net/go/mautrix v0.28.2-0.20260702123919-bf2dbb2aa895/go.mod h1:mWXQNmOlrq4VTDU9f1HO03BSIswdUIyyY4wUKHqwzzY=
maunium.net/go/mautrix v0.26.3 h1:tWZih6Vjw0qGTWuPmg9JUrQPzViTNDPGQLVc5UXC4nk=
maunium.net/go/mautrix v0.26.3/go.mod h1:v5ZdDoCwUpNqEj5OrhEoUa3L1kEddKPaAya9TgGXN38=

View file

@ -63,6 +63,11 @@ func (wa *WhatsAppClient) historySyncLoop(ctx context.Context) {
for {
var resetTimer bool
select {
case evt := <-wa.historySyncs:
// The timer is stopped unconditionally and restarted if either handleWAHistorySync had conversations,
// or if the timer was previously started and hadn't reached the loop above yet.
dispatchTimer.Stop()
resetTimer, _ = wa.handleWAHistorySync(ctx, evt, false)
case <-wa.historySyncWakeup:
dispatchTimer.Stop()
notif, rowid, err := wa.Main.DB.HSNotif.GetNext(ctx, wa.UserLogin.ID)
@ -115,30 +120,14 @@ func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context,
Uint32("chunk_order", evt.GetChunkOrder()).
Uint32("progress", evt.GetProgress()).
Logger()
log.Debug().
Int64("oldest_msg_in_chunk_ts", evt.GetOldestMsgInChunkTimestampSec()).
Any("full_request_meta", evt.GetFullHistorySyncOnDemandRequestMetadata()).
Any("access_status", evt.GetMessageAccessStatus()).
Str("peer_data_request_session_id", evt.GetPeerDataRequestSessionID()).
Msg("Downloading history sync")
log.Debug().Msg("Downloading history sync")
blob, err := wa.Client.DownloadHistorySync(log.WithContext(ctx), evt, true)
if err != nil {
log.Err(err).Msg("Failed to download history sync")
return
}
if blob.GetSyncType() == waHistorySync.HistorySync_ON_DEMAND {
wa.handleOnDemandHistorySync(ctx, blob)
if err = wa.Main.DB.HSNotif.Delete(ctx, rowid); err != nil {
log.Err(err).Msg("Failed to delete queued on-demand history sync notification")
} else if err = wa.Client.DeleteMedia(ctx, whatsmeow.MediaHistory, evt.GetDirectPath(), evt.GetFileEncSHA256(), evt.GetEncHandle()); err != nil {
log.Err(err).Msg("Failed to delete history sync blob from server")
} else {
log.Debug().Msg("Finished handling on-demand history sync and deleted history sync blob from server")
}
return
}
err = wa.Main.DB.DoTxn(ctx, nil, func(ctx context.Context) (innerErr error) {
innerErr = wa.handleWAHistorySync(ctx, evt, blob, true)
resetTimer, innerErr = wa.handleWAHistorySync(ctx, blob, true)
if innerErr != nil {
return
}
@ -150,28 +139,13 @@ func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context,
})
if err != nil {
log.Err(err).Msg("Failed to store history sync notification data")
} else {
resetTimer = blob.GetSyncType() == waHistorySync.HistorySync_INITIAL_BOOTSTRAP ||
blob.GetSyncType() == waHistorySync.HistorySync_RECENT ||
blob.GetSyncType() == waHistorySync.HistorySync_FULL
err = wa.Client.DeleteMedia(ctx, whatsmeow.MediaHistory, evt.GetDirectPath(), evt.GetFileEncSHA256(), evt.GetEncHandle())
if err != nil {
log.Err(err).Msg("Failed to delete history sync blob from server")
} else {
log.Debug().Msg("Deleted history sync blob from server")
}
}
return
}
func (wa *WhatsAppClient) handleWAHistorySync(
ctx context.Context,
notif *waE2E.HistorySyncNotification,
evt *waHistorySync.HistorySync,
stopOnError bool,
) error {
func (wa *WhatsAppClient) handleWAHistorySync(ctx context.Context, evt *waHistorySync.HistorySync, stopOnError bool) (bool, error) {
if evt == nil || evt.SyncType == nil {
return nil
return false, nil
}
log := wa.UserLogin.Log.With().
Str("action", "store history sync").
@ -196,16 +170,11 @@ func (wa *WhatsAppClient) handleWAHistorySync(
Int("recent_sticker_count", len(evt.GetRecentStickers())).
Int("past_participant_count", len(evt.GetPastParticipants())).
Msg("Ignoring history sync")
return nil
return false, nil
}
log.Info().
Int("conversation_count", len(evt.GetConversations())).
Int("past_participant_count", len(evt.GetPastParticipants())).
Dict("notification_metadata", zerolog.Dict().
Int64("oldest_msg_in_chunk_ts", notif.GetOldestMsgInChunkTimestampSec()).
Any("full_request_meta", notif.GetFullHistorySyncOnDemandRequestMetadata()).
Any("access_status", notif.GetMessageAccessStatus()).
Str("peer_data_request_session_id", notif.GetPeerDataRequestSessionID())).
Msg("Storing history sync")
start := time.Now()
successfullySavedTotal := 0
@ -246,7 +215,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
return c.Stringer("chat_jid", jid)
})
var minTime, maxTime, firstItemTime, lastItemTime time.Time
var minTime, maxTime time.Time
var minTimeIndex, maxTimeIndex int
ignoredTypes := 0
@ -260,15 +229,8 @@ func (wa *WhatsAppClient) handleWAHistorySync(
Str("msg_id", rawMsg.GetMessage().GetKey().GetID()).
Uint64("msg_time_seconds", rawMsg.GetMessage().GetMessageTimestamp()).
Msg("Dropping historical message due to parse error")
log.Trace().
Any("web_message_info", rawMsg.GetMessage()).
Msg("Content of event that failed to parse")
continue
}
if firstItemTime.IsZero() {
firstItemTime = msgEvt.Info.Timestamp
}
lastItemTime = msgEvt.Info.Timestamp
if minTime.IsZero() || msgEvt.Info.Timestamp.Before(minTime) {
minTime = msgEvt.Info.Timestamp
minTimeIndex = i
@ -301,9 +263,6 @@ func (wa *WhatsAppClient) handleWAHistorySync(
Int("lowest_time_index", minTimeIndex).
Time("highest_time", maxTime).
Int("highest_time_index", maxTimeIndex).
Time("first_item_time", firstItemTime).
Time("last_item_time", lastItemTime).
Bool("highest_time_mismatch", firstItemTime != maxTime).
Dict("metadata", zerolog.Dict().
Uint32("ephemeral_expiration", conv.GetEphemeralExpiration()).
Int64("ephemeral_setting_timestamp", conv.GetEphemeralSettingTimestamp()).
@ -312,9 +271,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
Bool("archived", conv.GetArchived()).
Uint32("pinned", conv.GetPinned()).
Uint64("mute_end", conv.GetMuteEndTime()).
Uint32("unread_count", conv.GetUnreadCount()).
Bool("end_of_history", conv.GetEndOfHistoryTransfer()).
Stringer("end_of_history_type", conv.GetEndOfHistoryTransferType()),
Uint32("unread_count", conv.GetUnreadCount()),
).
Msg("Collected messages to save from history sync conversation")
@ -322,7 +279,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
err = wa.Main.DB.Conversation.Put(ctx, wadb.NewConversation(wa.UserLogin.ID, jid, conv, maxTime))
if err != nil {
if stopOnError {
return fmt.Errorf("failed to save conversation metadata for %s: %w", jid, err)
return false, fmt.Errorf("failed to save conversation metadata for %s: %w", jid, err)
}
log.Err(err).Msg("Failed to save conversation metadata")
continue
@ -330,7 +287,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
err = wa.Main.DB.Message.Put(ctx, wa.UserLogin.ID, jid, messages)
if err != nil {
if stopOnError {
return fmt.Errorf("failed to save messages in %s: %w", jid, err)
return false, fmt.Errorf("failed to save messages in %s: %w", jid, err)
}
log.Err(err).Msg("Failed to save messages")
failedToSaveTotal += len(messages)
@ -340,7 +297,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
err = wa.Main.Bridge.DB.BackfillTask.MarkNotDone(ctx, wa.makeWAPortalKey(jid), wa.UserLogin.ID)
if err != nil {
if stopOnError {
return fmt.Errorf("failed to mark backfill task as not done for %s: %w", jid, err)
return false, fmt.Errorf("failed to mark backfill task as not done for %s: %w", jid, err)
}
log.Err(err).Msg("Failed to mark backfill task as not done")
}
@ -352,7 +309,9 @@ func (wa *WhatsAppClient) handleWAHistorySync(
Int("total_message_count", totalMessageCount).
Dur("duration", time.Since(start)).
Msg("Finished storing history sync")
return nil
resetTimer := evt.GetSyncType() == waHistorySync.HistorySync_RECENT ||
evt.GetSyncType() == waHistorySync.HistorySync_FULL
return resetTimer, nil
}
func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) {
@ -400,8 +359,8 @@ func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) {
return
}
wrappedInfo, err := wa.getChatInfo(ctx, conv.ChatJID, conv, true)
if errors.Is(err, whatsmeow.ErrNotInGroup) || errors.Is(err, whatsmeow.ErrGroupNotFound) {
log.Debug().Err(err).Stringer("chat_jid", conv.ChatJID).
if errors.Is(err, whatsmeow.ErrNotInGroup) {
log.Debug().Stringer("chat_jid", conv.ChatJID).
Msg("Skipping creating room because the user is not a participant")
//err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, conv.ChatJID)
//if err != nil {
@ -476,67 +435,38 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
}
var markRead bool
var startTime, endTime *time.Time
var conv *wadb.Conversation
if params.Forward || wa.Main.Config.HistorySync.BackwardsOnDemand {
conv, err = wa.Main.DB.Conversation.Get(ctx, wa.UserLogin.ID, portalJID)
if err != nil {
return nil, fmt.Errorf("failed to get conversation from database: %w", err)
}
}
if params.Forward {
if params.AnchorMessage != nil {
startTime = ptr.Ptr(params.AnchorMessage.Timestamp)
}
if conv != nil {
conv, err := wa.Main.DB.Conversation.Get(ctx, wa.UserLogin.ID, portalJID)
if err != nil {
return nil, fmt.Errorf("failed to get conversation from database: %w", err)
} else if conv != nil {
markRead = !ptr.Val(conv.MarkedAsUnread) && ptr.Val(conv.UnreadCount) == 0
}
} else {
if params.AnchorMessage != nil {
endTime = ptr.Ptr(params.AnchorMessage.Timestamp)
} else if params.Cursor != "" {
endTimeUnix, err := strconv.ParseInt(string(params.Cursor), 10, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse cursor: %w", err)
}
if params.Cursor != "" {
endTimeUnix, err := strconv.ParseInt(string(params.Cursor), 10, 64)
if err != nil {
return nil, fmt.Errorf("failed to parse cursor: %w", err)
}
cursorTime := time.Unix(endTimeUnix, 0)
if endTime == nil || cursorTime.Before(*endTime) {
endTime = &cursorTime
}
}
}
var anchorID types.MessageID
if params.AnchorMessage != nil {
parsedID, _ := waid.ParseMessageID(params.AnchorMessage.ID)
if parsedID != nil {
anchorID = parsedID.ID
}
}
var hasMore bool
if !params.Forward && wa.Main.Config.HistorySync.BackwardsOnDemand {
hasMore = conv != nil && ptr.Val(conv.EndOfHistoryTransferType) == waHistorySync.Conversation_COMPLETE_BUT_MORE_MESSAGES_REMAIN_ON_PRIMARY
endTime = ptr.Ptr(time.Unix(endTimeUnix, 0))
} else if params.AnchorMessage != nil {
endTime = ptr.Ptr(params.AnchorMessage.Timestamp)
}
messages, err := wa.Main.DB.Message.GetBetween(ctx, wa.UserLogin.ID, portalJID, startTime, endTime, params.Count+1)
if err != nil {
return nil, fmt.Errorf("failed to load messages from database: %w", err)
} else if len(messages) == 0 || (len(messages) == 1 && anchorID != "" && messages[0].GetKey().GetID() == anchorID) {
wa.deleteHistorySyncMessages(ctx, portalJID, 0, 0)
if hasMore && !params.AllowSlowFetch {
return &bridgev2.FetchMessagesResponse{
MoreRequiresSlowFetch: true,
HasMore: true,
Forward: params.Forward,
}, nil
} else if hasMore {
return wa.fetchMessagesFromPhone(ctx, params)
}
} else if len(messages) == 0 {
return &bridgev2.FetchMessagesResponse{
HasMore: false,
Forward: params.Forward,
}, nil
}
hasMore := false
oldestTS := messages[len(messages)-1].GetMessageTimestamp()
newestTS := messages[0].GetMessageTimestamp()
if len(messages) > params.Count {
oldestTS := messages[len(messages)-1].GetMessageTimestamp()
hasMore = true
// For safety, cut off messages with the oldest timestamp in the response.
// Otherwise, if there are multiple messages with the same timestamp, the next fetch may miss some.
@ -547,81 +477,19 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
}
}
}
resp, err := wa.convertHistorySyncMessages(ctx, params.Portal, portalJID, messages, true)
if err != nil {
return nil, fmt.Errorf("failed to convert messages: %w", err)
}
resp.HasMore = hasMore
resp.Forward = params.Forward
resp.MarkRead = markRead
return resp, nil
}
func (wa *WhatsAppClient) deleteHistorySyncMessages(ctx context.Context, portalJID types.JID, newestTS, oldestTS uint64) {
var err error
var rows int64
if (newestTS == 0 && oldestTS == 0) || !wa.Main.Bridge.Config.Backfill.Queue.AnyEnabled() {
// If the backfill queue isn't enabled, delete all messages after backfilling a batch.
rows, err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, portalJID)
} else {
// Otherwise just delete the messages that got backfilled
rows, err = wa.Main.DB.Message.DeleteBetween(ctx, wa.UserLogin.ID, portalJID, newestTS, oldestTS)
}
if err != nil {
zerolog.Ctx(ctx).Warn().Err(err).
Stringer("portal_jid", portalJID).
Uint64("newest_ts", newestTS).
Uint64("oldest_ts", oldestTS).
Msg("Failed to delete messages from database after backfill")
} else {
zerolog.Ctx(ctx).Debug().
Stringer("portal_jid", portalJID).
Uint64("newest_ts", newestTS).
Uint64("oldest_ts", oldestTS).
Int64("rows_affected", rows).
Msg("Deleted history sync messages from database")
}
}
func (wa *WhatsAppClient) convertHistorySyncMessages(
ctx context.Context,
portal *bridgev2.Portal,
portalJID types.JID,
messages []*waWeb.WebMessageInfo,
explodeOnError bool,
) (*bridgev2.FetchMessagesResponse, error) {
oldestTS := messages[len(messages)-1].GetMessageTimestamp()
newestTS := messages[0].GetMessageTimestamp()
convertedMessages := make([]*bridgev2.BackfillMessage, 0, len(messages))
convertedMessages := make([]*bridgev2.BackfillMessage, len(messages))
var mediaRequests []*wadb.MediaRequest
for i, msg := range messages {
evt, err := wa.Client.ParseWebMessage(portalJID, msg)
if err != nil {
if explodeOnError {
// This should never happen because the info is already parsed once before being stored in the database
return nil, fmt.Errorf("failed to parse info of message %s: %w", msg.GetKey().GetID(), err)
}
zerolog.Ctx(ctx).Warn().Err(err).
Int("msg_index", i).
Str("msg_id", msg.GetKey().GetID()).
Uint64("msg_time_seconds", msg.GetMessageTimestamp()).
Msg("Dropping historical message due to parse error")
zerolog.Ctx(ctx).Trace().
Any("web_message_info", msg.GetMessage()).
Msg("Content of event that failed to parse")
continue
}
if !explodeOnError {
msgType := getMessageType(evt.Message)
if msgType == "ignore" || strings.HasPrefix(msgType, "unknown_protocol_") {
continue
}
// This should never happen because the info is already parsed once before being stored in the database
return nil, fmt.Errorf("failed to parse info of message %s: %w", msg.GetKey().GetID(), err)
}
var mediaReq *wadb.MediaRequest
isViewOnce := evt.IsViewOnce || evt.IsViewOnceV2 || evt.IsViewOnceV2Extension
converted, mediaReq := wa.convertHistorySyncMessage(
ctx, portal, &evt.Info, evt.Message, evt.RawMessage, isViewOnce, msg.Reactions,
convertedMessages[i], mediaReq = wa.convertHistorySyncMessage(
ctx, params.Portal, &evt.Info, evt.Message, evt.RawMessage, isViewOnce, msg.Reactions,
)
convertedMessages = append(convertedMessages, converted)
if mediaReq != nil {
mediaRequests = append(mediaRequests, mediaReq)
}
@ -630,10 +498,24 @@ func (wa *WhatsAppClient) convertHistorySyncMessages(
return &bridgev2.FetchMessagesResponse{
Messages: convertedMessages,
Cursor: networkid.PaginationCursor(strconv.FormatUint(oldestTS, 10)),
HasMore: hasMore,
Forward: endTime == nil,
MarkRead: markRead,
// TODO set remaining or total count
CompleteCallback: func() {
// TODO this only deletes after backfilling. If there's no need for backfill after a relogin,
// the messages will be stuck in the database
wa.deleteHistorySyncMessages(ctx, portalJID, newestTS, oldestTS)
var err error
if !wa.Main.Bridge.Config.Backfill.Queue.Enabled && !wa.Main.Bridge.Config.Backfill.WillPaginateManually {
// If the backfill queue isn't enabled, delete all messages after backfilling a batch.
err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, portalJID)
} else {
// Otherwise just delete the messages that got backfilled
err = wa.Main.DB.Message.DeleteBetween(ctx, wa.UserLogin.ID, portalJID, newestTS, oldestTS)
}
if err != nil {
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to delete messages from database after backfill")
}
if len(mediaRequests) > 0 {
go func(ctx context.Context) {
for _, req := range mediaRequests {
@ -651,94 +533,6 @@ func (wa *WhatsAppClient) convertHistorySyncMessages(
}, nil
}
func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, params bridgev2.FetchMessagesParams) (*bridgev2.FetchMessagesResponse, error) {
if params.AnchorMessage == nil {
return nil, fmt.Errorf("anchor message is required to fetch messages from phone")
}
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: parsed.Chat,
Sender: parsed.Sender,
IsFromMe: parsed.Sender.ToNonAD() == wa.JID.ToNonAD() || parsed.Sender.ToNonAD() == wa.Device.GetLID().ToNonAD(),
IsGroup: parsed.Chat.Server == types.GroupServer,
},
ID: parsed.ID,
Timestamp: params.AnchorMessage.Timestamp,
}, 50)
zerolog.Ctx(ctx).Debug().
Str("request_msg_id", msgID).
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
}
portal, err := wa.Main.Bridge.GetPortalByKey(ctx, wa.makeWAPortalKey(portalJID))
if err != nil {
zerolog.Ctx(ctx).Err(err).Stringer("portal_jid", portalJID).Msg("Failed to get portal for on-demand history sync")
continue
}
ctx := zerolog.Ctx(ctx).With().
Str("portal_id", string(portal.ID)).
Str("portal_receiver", string(portal.Receiver)).
Stringer("portal_mxid", portal.MXID).
Logger().WithContext(ctx)
portal.HandleRemoteBackfill(ctx, wa.UserLogin, &simplevent.Backfill{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventBackfill,
PortalKey: portal.PortalKey,
},
GetDataFunc: func(ctx context.Context, portal *bridgev2.Portal) (*bridgev2.FetchMessagesResponse, error) {
if len(conv.GetMessages()) == 0 {
return &bridgev2.FetchMessagesResponse{}, nil
}
messages := make([]*waWeb.WebMessageInfo, len(conv.GetMessages()))
for i, rawMsg := range conv.GetMessages() {
messages[i] = rawMsg.Message
}
zerolog.Ctx(ctx).Debug().
Int("message_count", len(messages)).
Stringer("end_of_history_type", conv.GetEndOfHistoryTransferType()).
Msg("Converting messages to bridge from on-demand history sync")
resp, err := wa.convertHistorySyncMessages(ctx, portal, portalJID, messages, false)
if err != nil {
return nil, err
}
resp.HasMore = conv.GetEndOfHistoryTransferType() == waHistorySync.Conversation_COMPLETE_BUT_MORE_MESSAGES_REMAIN_ON_PRIMARY
return resp, nil
},
})
}
}
func (wa *WhatsAppClient) convertHistorySyncMessage(
ctx context.Context, portal *bridgev2.Portal, info *types.MessageInfo, msg, rawMsg *waE2E.Message, isViewOnce bool, reactions []*waWeb.Reaction,
) (*bridgev2.BackfillMessage, *wadb.MediaRequest) {

View file

@ -19,7 +19,6 @@ var WhatsAppGeneralCaps = &bridgev2.NetworkGeneralCapabilities{
AggressiveUpdateInfo: true,
ImplicitReadReceipts: true,
Provisioning: bridgev2.ProvisioningCapabilities{
ImagePackImport: true,
ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{
CreateDM: true,
LookupPhone: true,
@ -52,7 +51,7 @@ func (wa *WhatsAppConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilit
}
func (wa *WhatsAppConnector) GetBridgeInfoVersion() (info, caps int) {
return 1, 8
return 1, 7
}
const WAMaxFileSize = 2000 * 1024 * 1024
@ -67,7 +66,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel {
}
func capID() string {
base := "fi.mau.whatsapp.capabilities.2026_05_12"
base := "fi.mau.whatsapp.capabilities.2025_12_15"
if ffmpeg.Supported() {
return base + "+ffmpeg"
}
@ -126,10 +125,10 @@ var whatsappCaps = &event.RoomFeatures{
event.CapMsgSticker: {
MimeTypes: map[string]event.CapabilitySupportLevel{
"image/webp": event.CapLevelFullySupported,
// TODO see if sending lottie is possible
//"video/lottie+json": event.CapLevelFullySupported,
"image/png": event.CapLevelPartialSupport,
"image/jpeg": event.CapLevelPartialSupport,
// This will only be accepted if it was imported from WhatsApp
"video/lottie+json": event.CapLevelPartialSupport,
},
Caption: event.CapLevelDropped,
MaxSize: WAMaxFileSize,

View file

@ -257,7 +257,7 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
Name: ptr.Ptr(info.Name),
Topic: ptr.Ptr(info.Topic),
Members: &bridgev2.ChatMemberList{
IsFull: !info.IsIncognito && !info.IsParent,
IsFull: !info.IsIncognito,
TotalMemberCount: len(info.Participants),
MemberMap: make(map[networkid.UserID]bridgev2.ChatMember, len(info.Participants)),
PowerLevels: &bridgev2.PowerLevelOverrides{
@ -284,15 +284,11 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
},
ExtraUpdates: extraUpdater,
}
var hasSelf bool
for _, pcp := range info.Participants {
member := bridgev2.ChatMember{
EventSender: wa.makeEventSender(ctx, pcp.JID),
Membership: event.MembershipJoin,
}
if member.EventSender.IsFromMe {
hasSelf = true
}
if pcp.IsSuperAdmin {
member.PowerLevel = ptr.Ptr(superAdminPL)
} else if pcp.IsAdmin {
@ -303,21 +299,18 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
member.MemberEventExtra = map[string]any{
"com.beeper.exclude_from_timeline": true,
}
wrapped.Members.MemberMap.Set(member)
wrapped.Members.MemberMap[waid.MakeUserID(pcp.JID)] = member
if pcp.JID.Server == types.HiddenUserServer && !pcp.PhoneNumber.IsEmpty() {
wrapped.Members.MemberMap.Add(bridgev2.ChatMember{
wrapped.Members.MemberMap[waid.MakeUserID(pcp.PhoneNumber)] = bridgev2.ChatMember{
EventSender: bridgev2.EventSender{Sender: waid.MakeUserID(pcp.PhoneNumber)},
Membership: event.MembershipLeave,
PrevMembership: event.MembershipJoin,
MemberEventExtra: map[string]any{
"com.beeper.exclude_from_timeline": true,
},
})
}
}
}
if info.IsParent && !hasSelf && info.AddressingMode == types.AddressingModeLID {
wrapped.Members.MemberMap.Add(bridgev2.ChatMember{EventSender: wa.makeEventSender(ctx, wa.Device.LID)})
}
if !info.LinkedParentJID.IsEmpty() {
wrapped.ParentID = ptr.Ptr(waid.MakePortalID(info.LinkedParentJID))
@ -457,7 +450,7 @@ func (wa *WhatsAppClient) makePortalAvatarFetcher(avatarID string, sender types.
wrappedAvatar = &bridgev2.Avatar{
ID: networkid.AvatarID(avatar.ID),
Get: func(ctx context.Context) ([]byte, error) {
return wa.Client.DownloadMediaWithOnlyPath(ctx, avatar.DirectPath)
return wa.Client.DownloadMediaWithPath(ctx, avatar.DirectPath, nil, nil, nil, 0, "", "")
},
}
}
@ -496,7 +489,7 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne
if info.ThreadMeta.Picture != nil {
avatar.ID = networkid.AvatarID(info.ThreadMeta.Picture.ID)
avatar.Get = func(ctx context.Context) ([]byte, error) {
return wa.Client.DownloadMediaWithOnlyPath(ctx, info.ThreadMeta.Picture.DirectPath)
return wa.Client.DownloadMediaWithPath(ctx, info.ThreadMeta.Picture.DirectPath, nil, nil, nil, 0, "", "")
}
} else if info.ThreadMeta.Preview.ID != "" {
avatar.ID = networkid.AvatarID(info.ThreadMeta.Preview.ID)
@ -507,7 +500,7 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne
} else if meta.ThreadMeta.Picture == nil {
return nil, fmt.Errorf("full res avatar info is missing")
}
return wa.Client.DownloadMediaWithOnlyPath(ctx, meta.ThreadMeta.Picture.DirectPath)
return wa.Client.DownloadMediaWithPath(ctx, meta.ThreadMeta.Picture.DirectPath, nil, nil, nil, 0, "", "")
}
} else {
avatar.ID = "remove"

View file

@ -29,6 +29,7 @@ import (
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/appstate"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/proto/waHistorySync"
"go.mau.fi/whatsmeow/proto/waWa6"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/types"
@ -38,7 +39,6 @@ import (
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/bridgev2/status"
"maunium.net/go/mautrix/event"
"go.mau.fi/mautrix-whatsapp/pkg/waid"
)
@ -49,6 +49,7 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2.
UserLogin: login,
MC: noopMCInstance,
historySyncs: make(chan *waHistorySync.HistorySync, 64),
historySyncWakeup: make(chan struct{}, 1),
resyncQueue: make(map[types.JID]resyncQueueItem),
directMediaRetries: make(map[networkid.MessageID]*directMediaRetry),
@ -106,6 +107,7 @@ type WhatsAppClient struct {
JID types.JID
MC mClient
historySyncs chan *waHistorySync.HistorySync
historySyncWakeup chan struct{}
stopLoops atomic.Pointer[context.CancelFunc]
resyncQueue map[types.JID]resyncQueueItem
@ -129,7 +131,6 @@ var (
_ bridgev2.PushableNetworkAPI = (*WhatsAppClient)(nil)
_ bridgev2.BackgroundSyncingNetworkAPI = (*WhatsAppClient)(nil)
_ bridgev2.ChatViewingNetworkAPI = (*WhatsAppClient)(nil)
_ bridgev2.StickerImportingNetworkAPI = (*WhatsAppClient)(nil)
)
var pushCfg = &bridgev2.PushConfig{
@ -210,7 +211,6 @@ func (wa *WhatsAppClient) Connect(ctx context.Context) {
wa.Client.BackgroundEventCtx = wa.UserLogin.Log.WithContext(wa.Main.Bridge.BackgroundCtx)
zerolog.Ctx(ctx).Debug().Msg("Connecting to WhatsApp")
if err := wa.Client.ConnectContext(ctx); err != nil {
wa.callStopLoops()
zerolog.Ctx(ctx).Err(err).Msg("Failed to connect to WhatsApp")
state := status.BridgeState{
StateEvent: status.StateUnknownError,
@ -266,9 +266,7 @@ func (wa *WhatsAppClient) ConnectBackground(ctx context.Context, params *bridgev
return payload
}
defer func() {
if cli := wa.Client; cli != nil {
cli.GetClientPayload = nil
}
wa.Client.GetClientPayload = nil
}()
err := wa.Client.ConnectContext(ctx)
if err != nil {
@ -337,7 +335,6 @@ func (wa *WhatsAppClient) startLoops() {
if oldStop != nil {
(*oldStop)()
}
ctx = wa.UserLogin.Log.WithContext(ctx)
go wa.historySyncLoop(ctx)
go wa.ghostResyncLoop(ctx)
if mrc := wa.Main.Config.HistorySync.MediaRequests; mrc.AutoRequestMedia && mrc.RequestMethod == MediaRequestMethodLocalTime {
@ -355,14 +352,10 @@ func (wa *WhatsAppClient) GetStore() *store.Device {
return store.NoopDevice
}
func (wa *WhatsAppClient) callStopLoops() {
func (wa *WhatsAppClient) Disconnect() {
if stopHistorySyncLoop := wa.stopLoops.Swap(nil); stopHistorySyncLoop != nil {
(*stopHistorySyncLoop)()
}
}
func (wa *WhatsAppClient) Disconnect() {
wa.callStopLoops()
if cli := wa.Client; cli != nil {
cli.Disconnect()
}
@ -469,12 +462,3 @@ func (wa *WhatsAppClient) updatePresence(ctx context.Context, presence types.Pre
}
return err
}
func (wa *WhatsAppClient) DownloadImagePack(ctx context.Context, url string) (*bridgev2.ImportedImagePack, error) {
return wa.Main.MsgConv.DownloadImagePack(ctx, wa.UserLogin.ID, wa.Client, url)
}
func (wa *WhatsAppClient) ListImagePacks(ctx context.Context) ([]*event.ImagePackMetadata, error) {
// TODO
return nil, nil
}

View file

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

View file

@ -17,7 +17,6 @@
package connector
import (
"bytes"
"context"
"encoding/json"
"errors"
@ -68,8 +67,6 @@ func (wa *WhatsAppConnector) Download(ctx context.Context, mediaID networkid.Med
return wa.downloadMessageDirectMedia(ctx, parsedID, params)
} else if parsedID.Avatar != nil {
return wa.downloadAvatarDirectMedia(ctx, parsedID, params)
} else if parsedID.Sticker != nil {
return wa.downloadStickerDirectMedia(ctx, parsedID, params)
} else {
return nil, fmt.Errorf("unexpected media ID parsing result")
}
@ -131,30 +128,15 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars
}
return &mediaproxy.GetMediaResponseFile{
Callback: func(w *os.File) (*mediaproxy.FileMeta, error) {
return &mediaproxy.FileMeta{}, waClient.Client.DownloadMediaWithOnlyPathToFile(ctx, cachedInfo.DirectPath, w)
return &mediaproxy.FileMeta{}, waClient.Client.DownloadMediaWithPathToFile(
ctx, cachedInfo.DirectPath, nil, nil, nil, 0, "", "", w,
)
},
}, nil
}
func (wa *WhatsAppConnector) downloadStickerDirectMedia(ctx context.Context, parsedID *waid.ParsedMediaID, params map[string]string) (mediaproxy.GetMediaResponse, error) {
ul := wa.Bridge.GetCachedUserLoginByID(parsedID.UserLogin)
if ul == nil {
return nil, fmt.Errorf("%w: user login %s not found", bridgev2.ErrNotLoggedIn, parsedID.UserLogin)
}
waClient := ul.Client.(*WhatsAppClient)
if waClient.Client == nil {
return nil, fmt.Errorf("no WhatsApp client found on login %s", parsedID.UserLogin)
}
sticker, err := wa.MsgConv.GetCachedSticker(ctx, waClient.Client, parsedID.Sticker.PackID, parsedID.Sticker.FileHash)
if err != nil {
return nil, err
} else if sticker == nil {
return nil, mautrix.MNotFound.WithMessage("Sticker not found in pack")
}
return wa.makeDirectMediaResponse(ctx, waClient, sticker, sticker.MimeType, "", nil, params)
}
func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, parsedID *waid.ParsedMediaID, params map[string]string) (mediaproxy.GetMediaResponse, error) {
log := zerolog.Ctx(ctx)
msg, err := wa.Bridge.DB.Message.GetFirstPartByID(ctx, parsedID.UserLogin, parsedID.Message.String())
if err != nil {
return nil, fmt.Errorf("failed to get message: %w", err)
@ -170,9 +152,6 @@ func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, par
if err != nil {
return nil, fmt.Errorf("failed to unmarshal media keys: %w", err)
}
if version := parsedID.Message.Version; len(version) > 0 && !bytes.Equal(version, keys.EncSHA256) && !bytes.Equal(version, keys.SHA256) {
return nil, mautrix.MNotFound.WithMessage("Version mismatch, media was likely replaced")
}
var ul *bridgev2.UserLogin
if parsedID.UserLogin != "" {
ul = wa.Bridge.GetCachedUserLoginByID(parsedID.UserLogin)
@ -195,29 +174,16 @@ func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, par
if waClient.Client == nil {
return nil, fmt.Errorf("no WhatsApp client found on login")
}
return wa.makeDirectMediaResponse(ctx, waClient, keys, keys.MimeType, msg.ID, keys, params)
}
func (wa *WhatsAppConnector) makeDirectMediaResponse(
ctx context.Context,
waClient *WhatsAppClient,
dm whatsmeow.DownloadableMessage,
mimeType string,
msgID networkid.MessageID,
keys *msgconv.FailedMediaKeys,
params map[string]string,
) (mediaproxy.GetMediaResponse, error) {
return &mediaproxy.GetMediaResponseFile{
Callback: func(f *os.File) (*mediaproxy.FileMeta, error) {
log := zerolog.Ctx(ctx)
err := waClient.Client.DownloadToFile(ctx, dm, f)
if keys != nil && (errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith403) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith410) || errors.Is(err, whatsmeow.ErrNoURLPresent)) {
err := waClient.Client.DownloadToFile(ctx, keys, f)
if errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith403) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith410) || errors.Is(err, whatsmeow.ErrNoURLPresent) {
val := params["fi.mau.whatsapp.reload_media"]
if val == "false" || (!wa.Config.DirectMediaAutoRequest && val != "true") {
return nil, ErrReloadNeeded
}
log.Trace().Msg("Media not found for direct download, requesting and waiting")
err = waClient.requestAndWaitDirectMedia(ctx, msgID, keys)
err = waClient.requestAndWaitDirectMedia(ctx, msg.ID, keys)
if err != nil {
log.Trace().Err(err).Msg("Failed to wait for media for direct download")
return nil, err
@ -225,29 +191,30 @@ func (wa *WhatsAppConnector) makeDirectMediaResponse(
log.Trace().Msg("Retrying download after successful retry")
err = waClient.Client.DownloadToFile(ctx, keys, f)
}
if errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
if errors.Is(err, whatsmeow.ErrFileLengthMismatch) || errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
zerolog.Ctx(ctx).Warn().Err(err).Msg("Mismatching media checksums in message. Ignoring because WhatsApp seems to ignore them too")
} else if err != nil {
return nil, err
}
if mimeType == "application/was" {
mime := keys.MimeType
if mime == "application/was" {
if _, err := f.Seek(0, io.SeekStart); err != nil {
return nil, fmt.Errorf("failed to seek to start of sticker zip: %w", err)
} else if zipData, err := io.ReadAll(f); err != nil {
return nil, fmt.Errorf("failed to read sticker zip: %w", err)
} else if data, _, err := msgconv.ExtractAnimatedSticker(zipData); err != nil {
} else if data, err := msgconv.ExtractAnimatedSticker(zipData); err != nil {
return nil, fmt.Errorf("failed to extract animated sticker: %w %x", err, zipData)
} else if _, err := f.WriteAt(data, 0); err != nil {
return nil, fmt.Errorf("failed to write animated sticker to file: %w", err)
} else if err := f.Truncate(int64(len(data))); err != nil {
return nil, fmt.Errorf("failed to truncate animated sticker file: %w", err)
}
mimeType = "video/lottie+json"
mime = "video/lottie+json"
}
return &mediaproxy.FileMeta{
ContentType: mimeType,
ContentType: mime,
}, nil
},
}, nil

View file

@ -91,7 +91,6 @@ type WAMessageEvent struct {
parsedMessageType string
isUndecryptableUpsertSubEvent bool
dontRenderEdited bool
postHandle func()
}
@ -182,15 +181,12 @@ func (evt *WAMessageEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Por
}
var editedMsg *waE2E.Message
var previouslyConvertedPart *bridgev2.ConvertedMessagePart
targetMessage := evt.GetTargetMessage()
cacheMessage := targetMessage
if evt.isUndecryptableUpsertSubEvent {
// TODO db metadata needs to be updated in this case to remove the error
editedMsg = evt.Message
cacheMessage = evt.GetID()
} else {
editedMsg = evt.Message.GetProtocolMessage().GetEditedMessage()
previouslyConvertedPart = evt.wa.Main.GetMediaEditCache(portal, targetMessage)
previouslyConvertedPart = evt.wa.Main.GetMediaEditCache(portal, evt.GetTargetMessage())
meta := existing[0].Metadata.(*waid.MessageMetadata)
if slices.Contains(meta.Edits, evt.Info.ID) {
return nil, fmt.Errorf("%w: edit already handled", bridgev2.ErrIgnoringRemoteEvent)
@ -206,11 +202,9 @@ func (evt *WAMessageEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Por
evt.postHandle = func() {
evt.wa.processFailedMedia(ctx, portal.PortalKey, evt.GetID(), cm, false)
}
} else if len(cm.Parts) > 0 && cacheMessage != "" {
evt.wa.Main.AddMediaEditCache(portal, cacheMessage, cm.Parts[0])
}
editPart := cm.Parts[0].ToEditPart(existing[0])
if evt.isUndecryptableUpsertSubEvent || evt.dontRenderEdited {
if evt.isUndecryptableUpsertSubEvent {
if editPart.TopLevelExtra == nil {
editPart.TopLevelExtra = make(map[string]any)
}
@ -270,6 +264,18 @@ func (evt *WAMessageEvent) GetType() bridgev2.RemoteEventType {
func (evt *WAMessageEvent) HandleExisting(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, existing []*database.Message) (bridgev2.UpsertResult, error) {
if existing[0].Metadata.(*waid.MessageMetadata).Error == waid.MsgErrDecryptionFailed {
evt.wa.trackUndecryptableResolved(evt.MsgEvent)
if existing[0].HasFakeMXID() {
// The undecryptable message was hidden (decrypt_fail=hide), so no Matrix
// event was sent. Delete the placeholder DB entry and let the framework
// handle this as a brand-new message so a real Matrix event is created.
zerolog.Ctx(ctx).Debug().
Msg("Received decryptable version of previously hidden undecryptable message, re-handling as new message")
err := portal.Bridge.DB.Message.DeleteAllParts(ctx, portal.Receiver, evt.GetID())
if err != nil {
return bridgev2.UpsertResult{}, fmt.Errorf("failed to delete hidden placeholder message: %w", err)
}
return bridgev2.UpsertResult{ContinueMessageHandling: true}, nil
}
zerolog.Ctx(ctx).Debug().
Stringer("existing_mxid", existing[0].MXID).
Msg("Received decryptable version of previously undecryptable message")
@ -325,7 +331,8 @@ func (evt *WANowDecryptableMessage) GetType() bridgev2.RemoteEventType {
type WAUndecryptableMessage struct {
*MessageInfoWrapper
Type events.UnavailableType
Type events.UnavailableType
Hidden bool
}
var (
@ -372,9 +379,10 @@ func (evt *WAUndecryptableMessage) ConvertMessage(ctx context.Context, portal *b
// TODO thread root for comments
return &bridgev2.ConvertedMessage{
Parts: []*bridgev2.ConvertedMessagePart{{
Type: event.EventMessage,
Content: content,
Extra: extra,
Type: event.EventMessage,
Content: content,
Extra: extra,
DontBridge: evt.Hidden,
DBMetadata: &waid.MessageMetadata{
SenderDeviceID: evt.Info.Sender.Device,
Error: waid.MsgErrDecryptionFailed,

View file

@ -121,6 +121,3 @@ history_sync:
request_local_time: 120
# Maximum number of media request responses to handle in parallel per user.
max_async_handle: 2
# Use on-demand history sync requests for fetching older messages?
# This only applies when using the backfill queue, never for forward backfills.
backwards_on_demand: false

View file

@ -48,12 +48,11 @@ var (
_ bridgev2.DeleteChatHandlingNetworkAPI = (*WhatsAppClient)(nil)
)
func (wa *WhatsAppClient) HandleMatrixPollStart(ctx context.Context, msg *bridgev2.MatrixPollStart) (result *bridgev2.MatrixMessageResponse, retErr error) {
func (wa *WhatsAppClient) HandleMatrixPollStart(ctx context.Context, msg *bridgev2.MatrixPollStart) (*bridgev2.MatrixMessageResponse, error) {
waMsg, optionMap, err := wa.Main.MsgConv.PollStartToWhatsApp(ctx, msg.Content, msg.ReplyTo, msg.Portal)
if err != nil {
return nil, fmt.Errorf("failed to convert poll vote: %w", err)
}
defer wa.mcTrack(msg, time.Now(), &retErr)
resp, err := wa.handleConvertedMatrixMessage(ctx, &msg.MatrixMessage, waMsg, nil)
if err != nil {
return nil, err
@ -68,21 +67,19 @@ func (wa *WhatsAppClient) HandleMatrixPollStart(ctx context.Context, msg *bridge
return resp, nil
}
func (wa *WhatsAppClient) HandleMatrixPollVote(ctx context.Context, msg *bridgev2.MatrixPollVote) (result *bridgev2.MatrixMessageResponse, retErr error) {
func (wa *WhatsAppClient) HandleMatrixPollVote(ctx context.Context, msg *bridgev2.MatrixPollVote) (*bridgev2.MatrixMessageResponse, error) {
waMsg, err := wa.Main.MsgConv.PollVoteToWhatsApp(ctx, wa.Client, msg.Content, msg.VoteTo)
if err != nil {
return nil, fmt.Errorf("failed to convert poll vote: %w", err)
}
defer wa.mcTrack(msg, time.Now(), &retErr)
return wa.handleConvertedMatrixMessage(ctx, &msg.MatrixMessage, waMsg, nil)
}
func (wa *WhatsAppClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (result *bridgev2.MatrixMessageResponse, retErr error) {
func (wa *WhatsAppClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (*bridgev2.MatrixMessageResponse, error) {
waMsg, req, err := wa.Main.MsgConv.ToWhatsApp(ctx, wa.Client, msg.Event, msg.Content, msg.ReplyTo, msg.ThreadRoot, msg.Portal)
if err != nil {
return nil, fmt.Errorf("failed to convert message: %w", err)
}
defer wa.mcTrack(msg, time.Now(), &retErr)
return wa.handleConvertedMatrixMessage(ctx, msg, waMsg, req)
}
@ -110,7 +107,6 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg
wrappedMsgID2 := waid.MakeMessageID(chatJID, wa.GetStore().GetLID(), req.ID)
msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID))
msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID2))
zerolog.Ctx(ctx).Trace().Any("payload", waMsg).Msg("Outgoing message payload")
resp, err := wa.Client.SendMessage(ctx, chatJID, waMsg, *req)
if err != nil {
return nil, err
@ -157,7 +153,7 @@ func (wa *WhatsAppClient) PreHandleMatrixReaction(_ context.Context, msg *bridge
}, nil
}
func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (result *database.Reaction, retErr error) {
func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (*database.Reaction, error) {
messageID, err := waid.ParseMessageID(msg.TargetMessage.ID)
if err != nil {
return nil, fmt.Errorf("failed to parse target message ID: %w", err)
@ -174,7 +170,6 @@ func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev
SenderTimestampMS: proto.Int64(msg.Event.Timestamp),
},
}
defer wa.mcTrack(msg, time.Now(), &retErr)
var req whatsmeow.SendRequestExtra
if msg.Portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup {
reactionMsg.EncReactionMessage, err = wa.Client.EncryptReaction(ctx, msgconv.MessageIDToInfo(wa.Client, messageID), reactionMsg.ReactionMessage)
@ -196,7 +191,7 @@ func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev
}, err
}
func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) (retErr error) {
func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) error {
messageID, err := waid.ParseMessageID(msg.TargetReaction.MessageID)
if err != nil {
return fmt.Errorf("failed to parse target message ID: %w", err)
@ -220,13 +215,12 @@ func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *b
extra.ID = types.MessageID(msg.InputTransactionID)
}
defer wa.mcTrack(msg, time.Now(), &retErr)
resp, err := wa.Client.SendMessage(ctx, portalJID, reactionMsg, extra)
zerolog.Ctx(ctx).Trace().Any("response", resp).Msg("WhatsApp reaction response")
return err
}
func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.MatrixEdit) (retErr error) {
func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.MatrixEdit) error {
log := zerolog.Ctx(ctx)
var editID types.MessageID
@ -250,8 +244,6 @@ func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.M
if err != nil {
return fmt.Errorf("failed to convert message: %w", err)
}
defer wa.mcTrack(edit, time.Now(), &retErr)
convertedEdit := wa.Client.BuildEdit(messageID.Chat, messageID.ID, waMsg)
if edit.OrigSender == nil {
convertedEdit.EditedMessage.Message.ProtocolMessage.TimestampMS = proto.Int64(edit.Event.Timestamp)
@ -266,7 +258,7 @@ func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.M
return err
}
func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) (retErr error) {
func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) error {
log := zerolog.Ctx(ctx)
messageID, err := waid.ParseMessageID(msg.TargetMessage.ID)
if err != nil {
@ -278,7 +270,6 @@ func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *br
return fmt.Errorf("failed to parse portal ID: %w", err)
}
defer wa.mcTrack(msg, time.Now(), &retErr)
revokeMessage := wa.Client.BuildRevoke(messageID.Chat, messageID.Sender, messageID.ID)
extra := whatsmeow.SendRequestExtra{}
@ -291,7 +282,7 @@ func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *br
return err
}
func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bridgev2.MatrixReadReceipt) (retErr error) {
func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bridgev2.MatrixReadReceipt) error {
if !receipt.ReadUpTo.After(receipt.LastRead) {
return nil
}
@ -330,7 +321,6 @@ func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *
}
messagesToRead[key] = append(messagesToRead[key], parsed.ID)
}
defer wa.mcTrack(receipt, time.Now(), &retErr)
for messageSender, ids := range messagesToRead {
err = wa.Client.MarkRead(ctx, ids, receipt.Receipt.Timestamp, portalJID, messageSender)
if err != nil {
@ -340,7 +330,7 @@ func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *
return err
}
func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.MatrixTyping) (retErr error) {
func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.MatrixTyping) error {
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return err
@ -361,7 +351,6 @@ func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.
return nil
}
defer wa.mcTrack(msg, time.Now(), &retErr)
if wa.Main.Config.SendPresenceOnTyping {
err = wa.updatePresence(ctx, types.PresenceAvailable)
if err != nil {
@ -373,7 +362,7 @@ func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.
var errUnsupportedDisappearingTimer = bridgev2.WrapErrorInStatus(errors.New("invalid value for disappearing timer")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true)
func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg *bridgev2.MatrixDisappearingTimer) (ok bool, retErr error) {
func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg *bridgev2.MatrixDisappearingTimer) (bool, error) {
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return false, err
@ -385,7 +374,6 @@ func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg
return false, fmt.Errorf("%w (%s)", errUnsupportedDisappearingTimer, msg.Content.Timer.Duration)
}
defer wa.mcTrack(msg, time.Now(), &retErr)
settingTS := time.UnixMilli(msg.Event.Timestamp)
err = wa.Client.SetDisappearingTimer(ctx, portalJID, msg.Content.Timer.Duration, settingTS)
if err != nil {
@ -402,11 +390,7 @@ func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg
return true, nil
}
func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (result *bridgev2.MatrixMembershipResult, retErr error) {
if msg.Type.IsSelf && msg.OrigSender != nil {
return nil, nil
}
func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (*bridgev2.MatrixMembershipResult, error) {
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return nil, err
@ -446,7 +430,6 @@ func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridg
return nil, fmt.Errorf("cannot get target intent: unknown type: %T", target)
}
defer wa.mcTrack(msg, time.Now(), &retErr)
resp, err := wa.Client.UpdateGroupParticipants(ctx, portalJID, changes, action)
if err != nil {
return nil, err
@ -462,7 +445,7 @@ func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridg
return &bridgev2.MatrixMembershipResult{RedirectTo: waid.MakeUserID(resp[0].JID)}, nil
}
func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev2.MatrixRoomName) (ok bool, retErr error) {
func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev2.MatrixRoomName) (bool, error) {
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return false, err
@ -472,7 +455,6 @@ func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev
return false, fmt.Errorf("cannot set room name for DM")
}
defer wa.mcTrack(msg, time.Now(), &retErr)
err = wa.Client.SetGroupName(ctx, portalJID, msg.Content.Name)
if err != nil {
return false, err
@ -484,7 +466,7 @@ func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev
return true, nil
}
func (wa *WhatsAppClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridgev2.MatrixRoomTopic) (ok bool, retErr error) {
func (wa *WhatsAppClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridgev2.MatrixRoomTopic) (bool, error) {
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return false, err
@ -494,7 +476,6 @@ func (wa *WhatsAppClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridge
return false, fmt.Errorf("cannot set room topic for DM")
}
defer wa.mcTrack(msg, time.Now(), &retErr)
newID := wa.Client.GenerateMessageID()
oldID := msg.Portal.Metadata.(*waid.PortalMetadata).TopicID
err = wa.Client.SetGroupTopic(ctx, portalJID, oldID, newID, msg.Content.Topic)
@ -509,7 +490,7 @@ func (wa *WhatsAppClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridge
return true, nil
}
func (wa *WhatsAppClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridgev2.MatrixRoomAvatar) (ok bool, retErr error) {
func (wa *WhatsAppClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridgev2.MatrixRoomAvatar) (bool, error) {
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return false, err
@ -532,7 +513,6 @@ func (wa *WhatsAppClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridg
}
}
defer wa.mcTrack(msg, time.Now(), &retErr)
avatarID, err := wa.Client.SetGroupPhoto(ctx, portalJID, data)
if err != nil {
return false, err
@ -600,7 +580,7 @@ func convertRoomAvatar(data []byte) ([]byte, error) {
return buf.Bytes(), nil
}
func (wa *WhatsAppClient) HandleMute(ctx context.Context, msg *bridgev2.MatrixMute) (retErr error) {
func (wa *WhatsAppClient) HandleMute(ctx context.Context, msg *bridgev2.MatrixMute) error {
chatJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return err
@ -611,16 +591,14 @@ func (wa *WhatsAppClient) HandleMute(ctx context.Context, msg *bridgev2.MatrixMu
if !muted || mutedUntil == event.MutedForever {
muteTS = nil
}
defer wa.mcTrack(msg, time.Now(), &retErr)
return wa.Client.SendAppState(ctx, appstate.BuildMuteAbs(chatJID, muted, muteTS))
}
func (wa *WhatsAppClient) HandleRoomTag(ctx context.Context, msg *bridgev2.MatrixRoomTag) (retErr error) {
func (wa *WhatsAppClient) HandleRoomTag(ctx context.Context, msg *bridgev2.MatrixRoomTag) error {
chatJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return err
}
defer wa.mcTrack(msg, time.Now(), &retErr)
_, isFavorite := msg.Content.Tags[event.RoomTagFavourite]
return wa.Client.SendAppState(ctx, appstate.BuildPin(chatJID, isFavorite))
}
@ -652,7 +630,7 @@ func (wa *WhatsAppClient) getLastMessageInfo(ctx context.Context, chatJID types.
return lastTS, lastKey, nil
}
func (wa *WhatsAppClient) HandleMarkedUnread(ctx context.Context, msg *bridgev2.MatrixMarkedUnread) (retErr error) {
func (wa *WhatsAppClient) HandleMarkedUnread(ctx context.Context, msg *bridgev2.MatrixMarkedUnread) error {
chatJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return err
@ -661,11 +639,10 @@ func (wa *WhatsAppClient) HandleMarkedUnread(ctx context.Context, msg *bridgev2.
if err != nil {
return err
}
defer wa.mcTrack(msg, time.Now(), &retErr)
return wa.Client.SendAppState(ctx, appstate.BuildMarkChatAsRead(chatJID, msg.Content.Unread, lastTS, lastKey))
}
func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridgev2.MatrixDeleteChat) (retErr error) {
func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridgev2.MatrixDeleteChat) error {
chatJID, err := waid.ParsePortalID(msg.Portal.ID)
if err != nil {
return err
@ -686,6 +663,5 @@ func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridg
if err != nil {
return err
}
defer wa.mcTrack(msg, time.Now(), &retErr)
return wa.Client.SendAppState(ctx, appstate.BuildDeleteChat(chatJID, lastTS, lastKey, true))
}

View file

@ -28,6 +28,7 @@ import (
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/appstate"
"go.mau.fi/whatsmeow/proto/waE2E"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
"maunium.net/go/mautrix/bridgev2"
@ -76,7 +77,6 @@ func init() {
func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
log := wa.UserLogin.Log
ctx := log.WithContext(wa.Main.Bridge.BackgroundCtx)
wa.MC.OnWhatsAppEvent(rawEvt)
success = true
switch evt := rawEvt.(type) {
@ -111,7 +111,9 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
success = wa.handleWAPin(evt)
case *events.HistorySync:
wa.UserLogin.Log.Warn().Msg("Unexpected history sync event received")
if wa.Main.Bridge.Config.Backfill.Enabled {
wa.historySyncs <- evt.Data
}
case *events.MediaRetry:
wa.phoneSeen(evt.Timestamp)
success = wa.UserLogin.QueueRemoteEvent(&WAMediaRetry{MediaRetry: evt, wa: wa}).Success
@ -126,20 +128,6 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
success = wa.handleWANewsletterLeave(evt)
case *events.Picture:
success = wa.handleWAPictureUpdate(ctx, evt)
case *events.NotifyAccountReachoutTimelock:
wa.UserLogin.TrackAnalytics("WhatsApp Account Reachout Timelock", map[string]any{
"enforcement_type": evt.EnforcementType,
"is_active": evt.IsActive,
"time_enforcement_ends": evt.TimeEnforcementEnds.Time,
})
wa.UserLogin.Metadata.(*waid.UserLoginMetadata).ReachoutTimelockUntil = evt.TimeEnforcementEnds.Time
if wa.UserLogin.BridgeState.GetPrevUnsent().StateEvent == status.StateConnected {
wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
}
err := wa.UserLogin.Save(ctx)
if err != nil {
log.Err(err).Msg("Failed to save user login metadata after reachout timelock update")
}
case *events.AppStateSyncComplete:
wa.handleWAAppStateSyncComplete(ctx, evt)
@ -182,6 +170,7 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
}()
go wa.syncRemoteProfile(ctx, nil)
}
wa.MC.OnConnect(store.GetWAVersion()[2], wa.Device.Platform)
case *events.OfflineSyncPreview:
log.Info().
Int("message_count", evt.Messages).
@ -322,10 +311,52 @@ func (wa *WhatsAppClient) rerouteWAMessage(ctx context.Context, evtType string,
func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Message) (success bool) {
success = true
wa.rerouteWAMessage(ctx, "message", &evt.Info.MessageSource, evt.Info.ID)
wa.UserLogin.Log.Trace().
Any("info", evt.Info).
Any("payload", evt.Message).
Msg("Received WhatsApp message")
if evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast {
return
}
if evt.Info.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
}
parsedMessageType := getMessageType(evt.Message)
if parsedMessageType == "ignore" || strings.HasPrefix(parsedMessageType, "unknown_protocol_") {
return
}
if encReact := evt.Message.GetEncReactionMessage(); encReact != nil {
decrypted, err := wa.Client.DecryptReaction(ctx, evt)
if err != nil {
@ -347,66 +378,13 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
if encMessage := evt.Message.GetSecretEncryptedMessage(); encMessage != nil {
decrypted, err := wa.Client.DecryptSecretEncryptedMessage(ctx, evt)
if err != nil {
wa.UserLogin.Log.Err(err).
Str("message_id", evt.Info.ID).
Stringer("evt_sender", evt.Info.Sender).
Any("target_message_key", encMessage.TargetMessageKey).
Msg("Failed to decrypt secret-encrypted message")
wa.UserLogin.Log.Err(err).Str("message_id", evt.Info.ID).Msg("Failed to decrypt message")
return
}
evt.RawMessage = decrypted
evt.UnwrapRaw()
parsedMessageType = getMessageType(evt.Message)
}
wa.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.IsFromMe &&
evt.Message.GetProtocolMessage().GetHistorySyncNotification() != nil &&
wa.Main.Bridge.Config.Backfill.Enabled {
wa.saveWAHistorySyncNotification(ctx, evt.Message.ProtocolMessage.HistorySyncNotification)
}
if parsedMessageType == "ignore" || strings.HasPrefix(parsedMessageType, "unknown_protocol_") {
return
}
dontRenderEdited := false
messageAssoc := evt.Message.GetMessageContextInfo().GetMessageAssociation()
if assocType := messageAssoc.GetAssociationType(); assocType == waE2E.MessageAssociation_HD_IMAGE_DUAL_UPLOAD || assocType == waE2E.MessageAssociation_HD_VIDEO_DUAL_UPLOAD {
parentKey := messageAssoc.GetParentMessageKey()
protocolMsg := evt.Message.GetProtocolMessage()
if protocolMsg.GetType() != waE2E.ProtocolMessage_MESSAGE_EDIT || protocolMsg.GetKey() == nil {
protocolMsg = &waE2E.ProtocolMessage{
Type: waE2E.ProtocolMessage_MESSAGE_EDIT.Enum(),
Key: parentKey,
EditedMessage: evt.Message.GetAssociatedChildMessage().GetMessage(),
}
dontRenderEdited = true
} else if child := protocolMsg.GetEditedMessage().GetAssociatedChildMessage().GetMessage(); child != nil {
protocolMsg.EditedMessage = child
protocolMsg.Key = parentKey
}
wa.UserLogin.Log.Debug().
Str("message_id", evt.Info.ID).
Str("parent_id", parentKey.GetID()).
Stringer("assoc_type", assocType).
Msg("Received HD replacement message, converting to edit")
evt.Message = &waE2E.Message{
ProtocolMessage: protocolMsg,
}
parsedMessageType = getMessageType(evt.Message)
} else if assocType == waE2E.MessageAssociation_MOTION_PHOTO {
//evt.Message = evt.Message.GetAssociatedChildMessage().GetMessage()
wa.UserLogin.Log.Debug().
Str("message_id", evt.Info.ID).
Str("parent_id", messageAssoc.GetParentMessageKey().GetID()).
Msg("Ignoring motion photo update")
return
}
res := wa.UserLogin.QueueRemoteEvent(&WAMessageEvent{
MessageInfoWrapper: &MessageInfoWrapper{
Info: evt.Info,
@ -416,7 +394,6 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
MsgEvent: evt,
parsedMessageType: parsedMessageType,
dontRenderEdited: dontRenderEdited,
})
return res.Success
}
@ -429,18 +406,17 @@ func (wa *WhatsAppClient) handleWAUndecryptableMessage(ctx context.Context, evt
Str("decrypt_fail", string(evt.DecryptFailMode)).
Msg("Received undecryptable WhatsApp message")
wa.trackUndecryptable(evt)
if evt.DecryptFailMode == events.DecryptFailHide {
return true
}
if evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast {
return true
}
hidden := evt.DecryptFailMode == events.DecryptFailHide
res := wa.UserLogin.QueueRemoteEvent(&WAUndecryptableMessage{
MessageInfoWrapper: &MessageInfoWrapper{
Info: evt.Info,
wa: wa,
},
Type: evt.UnavailableType,
Type: evt.UnavailableType,
Hidden: hidden,
})
return res.Success
}
@ -530,7 +506,7 @@ func (wa *WhatsAppClient) handleWALogout(reason events.ConnectFailureReason, onC
} else if reason == events.ConnectFailureMainDeviceGone {
errorCode = WAMainDeviceGone
}
wa.Disconnect()
wa.Client.Disconnect()
wa.Client = nil
wa.JID = types.EmptyJID
wa.UserLogin.Metadata.(*waid.UserLoginMetadata).WADeviceID = 0

View file

@ -2,7 +2,6 @@ package connector
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
@ -13,7 +12,6 @@ import (
"go.mau.fi/util/exsync"
"go.mau.fi/util/jsontime"
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
waLog "go.mau.fi/whatsmeow/util/log"
"maunium.net/go/mautrix/bridgev2"
@ -27,7 +25,6 @@ const (
LoginStepIDQR = "fi.mau.whatsapp.login.qr"
LoginStepIDPhoneNumber = "fi.mau.whatsapp.login.phone"
LoginStepIDCode = "fi.mau.whatsapp.login.code"
LoginStepIDPasskey = "fi.mau.whatsapp.login.passkey"
LoginStepIDComplete = "fi.mau.whatsapp.login.complete"
LoginFlowIDQR = "qr"
@ -79,8 +76,8 @@ var (
}
ErrRateLimitedByWhatsApp = bridgev2.RespError{
ErrCode: "FI.MAU.WHATSAPP.RATE_LIMITED",
Err: "Rate limited by WhatsApp. Try again later.",
StatusCode: http.StatusBadRequest,
Err: "Rate limited by WhatsApp",
StatusCode: http.StatusTooManyRequests,
}
)
@ -94,11 +91,9 @@ func (wa *WhatsAppConnector) CreateLogin(_ context.Context, user *bridgev2.User,
Bool("phone_code", flowID == LoginFlowIDPhone).
Logger(),
WaitForQRs: exsync.NewEvent(),
LoginComplete: exsync.NewEvent(),
PasskeyRequest: exsync.NewEvent(),
PasskeyConfirmation: exsync.NewEvent(),
Received515: exsync.NewEvent(),
WaitForQRs: exsync.NewEvent(),
LoginComplete: exsync.NewEvent(),
Received515: exsync.NewEvent(),
}, nil
}
@ -119,11 +114,6 @@ type WALogin struct {
Received515 *exsync.Event
PrevQRIndex atomic.Int32
PasskeyRequest *exsync.Event
PasskeyRequestData *events.PairPasskeyRequest
PasskeyConfirmation *exsync.Event
PasskeyConfirmationData *events.PairPasskeyConfirmation
Closed atomic.Bool
EventHandlerID uint32
}
@ -132,7 +122,6 @@ var (
_ bridgev2.LoginProcessDisplayAndWait = (*WALogin)(nil)
_ bridgev2.LoginProcessUserInput = (*WALogin)(nil)
_ bridgev2.LoginProcessWithOverride = (*WALogin)(nil)
_ bridgev2.LoginProcessWebAuthn = (*WALogin)(nil)
)
const LoginConnectWait = 15 * time.Second
@ -273,17 +262,6 @@ func (wl *WALogin) handleEvent(rawEvt any) {
case *events.ClientOutdated:
wl.Log.Error().Msg("Got client outdated error")
wl.LoginError = ErrLoginClientOutdated
case *events.PairPasskeyRequest:
wl.PasskeyRequestData = evt
wl.PasskeyRequest.Set()
return
case *events.PairPasskeyConfirmation:
wl.PasskeyConfirmationData = evt
wl.PasskeyConfirmation.Set()
return
case *events.PairPasskeyError:
wl.Log.Err(evt.Error).Msg("Got passkey error")
wl.LoginError = evt.Error
case *events.PairSuccess:
wl.Log.Info().Any("event_data", evt).Msg("Got pair successful event")
wl.LoginSuccess = evt
@ -311,14 +289,10 @@ func (wl *WALogin) handleEvent(rawEvt any) {
func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
if wl.PhoneCode {
select {
case <-ctx.Done():
err := wl.LoginComplete.Wait(ctx)
if err != nil {
wl.Cancel()
return nil, ctx.Err()
case <-wl.PasskeyRequest.GetChan():
return wl.makePasskeyStep()
case <-wl.LoginComplete.GetChan():
// continue
return nil, err
}
} else {
prevIndex := int(wl.PrevQRIndex.Load())
@ -345,16 +319,9 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
case <-ctx.Done():
wl.Cancel()
return nil, ctx.Err()
case <-wl.PasskeyRequest.GetChan():
return wl.makePasskeyStep()
case <-wl.LoginComplete.GetChan():
// continue
}
}
return wl.onLoginComplete(ctx)
}
func (wl *WALogin) onLoginComplete(ctx context.Context) (*bridgev2.LoginStep, error) {
if wl.LoginError != nil {
wl.Log.Debug().Err(wl.LoginError).Msg("Login completed with error")
wl.Cancel()
@ -405,66 +372,6 @@ func (wl *WALogin) onLoginComplete(ctx context.Context) (*bridgev2.LoginStep, er
}, nil
}
func (wl *WALogin) makePasskeyStep() (*bridgev2.LoginStep, error) {
pubkeyData, err := json.Marshal(wl.PasskeyRequestData.PublicKey)
if err != nil {
wl.Log.Err(err).Msg("Failed to marshal public key data")
return nil, err
}
return &bridgev2.LoginStep{
Type: bridgev2.LoginStepTypeWebAuthn,
StepID: LoginStepIDPasskey,
WebAuthnParams: &bridgev2.LoginWebAuthnParams{
URL: "https://web.whatsapp.com",
PublicKey: pubkeyData,
},
}, nil
}
func (wl *WALogin) SubmitWebAuthnResponse(ctx context.Context, rawResp json.RawMessage) (*bridgev2.LoginStep, error) {
var resp types.WebAuthnResponse
err := json.Unmarshal(rawResp, &resp)
if err != nil {
wl.Log.Err(err).Msg("Failed to unmarshal WebAuthn response")
wl.Cancel()
return nil, err
}
err = wl.Client.SendPasskeyResponse(ctx, &resp)
if err != nil {
wl.Log.Err(err).Msg("Failed to send WebAuthn response")
wl.Cancel()
return nil, err
}
select {
case <-ctx.Done():
wl.Cancel()
return nil, ctx.Err()
case <-wl.PasskeyConfirmation.GetChan():
if !wl.PasskeyConfirmationData.SkipHandoffUX {
wl.Cancel()
return nil, bridgev2.RespError{
ErrCode: "FI.MAU.WHATSAPP.NOT_IMPLEMENTED",
Err: "Displaying WebAuthn pairing confirmation codes is not yet implemented",
StatusCode: http.StatusBadRequest,
}
}
err = wl.Client.SendPasskeyConfirmation(ctx)
if err != nil {
wl.Log.Err(err).Msg("Failed to send WebAuthn confirmation")
wl.Cancel()
return nil, err
}
case <-wl.LoginComplete.GetChan():
}
select {
case <-ctx.Done():
wl.Cancel()
return nil, ctx.Err()
case <-wl.LoginComplete.GetChan():
}
return wl.onLoginComplete(ctx)
}
func (wl *WALogin) Cancel() {
wl.Closed.Store(true)
wl.Client.RemoveEventHandler(wl.EventHandlerID)

View file

@ -21,7 +21,6 @@ import (
"encoding/json"
"time"
"go.mau.fi/util/ptr"
"go.mau.fi/whatsmeow"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/types"
@ -40,16 +39,14 @@ func (wa *WhatsAppClient) initMC() {
}
type mClient = interface {
OnMatrixEvent(any, time.Duration, error)
OnWhatsAppEvent(any)
OnConnect(version uint32, platform string)
}
type noopMC struct{}
var noopMCInstance mClient = &noopMC{}
func (n *noopMC) OnMatrixEvent(any, time.Duration, error) {}
func (n *noopMC) OnWhatsAppEvent(any) {}
func (n *noopMC) OnConnect(version uint32, platform string) {}
type mWAClient = interface {
MSend(data []byte)
@ -82,7 +79,3 @@ func (wa *WhatsAppClient) MSave(s json.RawMessage) {
wa.UserLogin.Log.Err(err).Msg("Failed to save MC data")
}
}
func (wa *WhatsAppClient) mcTrack(evt any, start time.Time, err *error) {
wa.MC.OnMatrixEvent(evt, time.Since(start), ptr.Val(err))
}

View file

@ -70,9 +70,10 @@ func (wa *WhatsAppClient) processFailedMedia(ctx context.Context, portalKey netw
func (wa *WhatsAppClient) mediaRequestLoop(ctx context.Context) {
log := wa.UserLogin.Log.With().Str("loop", "media requests").Logger()
ctx = log.WithContext(ctx)
userTz, err := wa.UserLogin.Metadata.(*waid.UserLoginMetadata).LoadTimezone()
tzName := wa.UserLogin.Metadata.(*waid.UserLoginMetadata).Timezone
userTz, err := time.LoadLocation(tzName)
var startIn time.Duration
if err == nil && userTz != nil {
if tzName != "" && err == nil && userTz != nil {
now := time.Now()
startAt := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, userTz)
startAt = startAt.Add(time.Duration(wa.Main.Config.HistorySync.MediaRequests.RequestLocalTime) * time.Minute)

View file

@ -37,13 +37,6 @@ func (wa *WhatsAppClient) FillBridgeState(state status.BridgeState) status.Bridg
state.Error = WAPhoneOffline
state.UserAction = status.UserActionOpenNative
}
rtu := wa.UserLogin.Metadata.(*waid.UserLoginMetadata).ReachoutTimelockUntil
if !rtu.IsZero() {
if state.Info == nil {
state.Info = make(map[string]any)
}
state.Info["reachout_timelock_until"] = rtu
}
return state
}

View file

@ -189,7 +189,7 @@ func (wa *WhatsAppClient) getContactList(ctx context.Context, filter string, onl
}
resp := make([]*bridgev2.ResolveIdentifierResponse, 0, len(contacts))
for jid, contactInfo := range contacts {
if onlyContacts && (contactInfo.FirstName == "" && contactInfo.FullName == "") {
if onlyContacts && contactInfo.FirstName == "" {
continue
}
if !matchesQuery(contactInfo.PushName, filter) && !matchesQuery(contactInfo.FullName, filter) && !matchesQuery(jid.User, filter) {

View file

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

View file

@ -116,12 +116,9 @@ func (mq *MessageQuery) GetBetween(ctx context.Context, loginID networkid.UserLo
AsList()
}
func (mq *MessageQuery) DeleteBetween(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID, before, after uint64) (int64, error) {
res, err := mq.Exec(ctx, deleteHistorySyncMessagesBetweenQuery, mq.BridgeID, loginID, chatJID, before, after)
if err != nil {
return 0, err
}
return res.RowsAffected()
func (mq *MessageQuery) DeleteBetween(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID, before, after uint64) error {
_, err := mq.Exec(ctx, deleteHistorySyncMessagesBetweenQuery, mq.BridgeID, loginID, chatJID, before, after)
return err
}
func (mq *MessageQuery) DeleteAll(ctx context.Context, loginID networkid.UserLoginID) error {
@ -129,12 +126,9 @@ func (mq *MessageQuery) DeleteAll(ctx context.Context, loginID networkid.UserLog
return err
}
func (mq *MessageQuery) DeleteAllInChat(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID) (int64, error) {
res, err := mq.Exec(ctx, deleteHistorySyncMessagesForPortalQuery, mq.BridgeID, loginID, chatJID)
if err != nil {
return 0, err
}
return res.RowsAffected()
func (mq *MessageQuery) DeleteAllInChat(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID) error {
_, err := mq.Exec(ctx, deleteHistorySyncMessagesForPortalQuery, mq.BridgeID, loginID, chatJID)
return err
}
func (mq *MessageQuery) ConversationHasMessages(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID) (exists bool, err error) {

View file

@ -130,8 +130,8 @@ func getMessageType(waMsg *waE2E.Message) string {
return "secret encrypted"
case waMsg.PollResultSnapshotMessage != nil:
return "poll result snapshot"
case waMsg.MessageHistoryNotice != nil:
return "message history notice"
case waMsg.MessageHistoryBundle != nil:
return "message history bundle"
case waMsg.RequestPhoneNumberMessage != nil:
return "request phone number"
case waMsg.KeepInChatMessage != nil:
@ -152,7 +152,7 @@ func getMessageType(waMsg *waE2E.Message) string {
return "chat"
case waMsg.PlaceholderMessage != nil:
return "placeholder"
case waMsg.SenderKeyDistributionMessage != nil, waMsg.StickerSyncRmrMessage != nil, waMsg.MessageHistoryBundle != nil:
case waMsg.SenderKeyDistributionMessage != nil, waMsg.StickerSyncRmrMessage != nil:
return "ignore"
default:
return "unknown"

View file

@ -19,7 +19,6 @@ package msgconv
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
@ -50,13 +49,7 @@ import (
"go.mau.fi/mautrix-whatsapp/pkg/waid"
)
func (mc *MessageConverter) generateContextInfo(
ctx context.Context,
replyTo *database.Message,
portal *bridgev2.Portal,
perMessageTimer *event.BeeperDisappearingTimer,
roomMention bool,
) *waE2E.ContextInfo {
func (mc *MessageConverter) generateContextInfo(ctx context.Context, replyTo *database.Message, portal *bridgev2.Portal, perMessageTimer *event.BeeperDisappearingTimer) *waE2E.ContextInfo {
contextInfo := &waE2E.ContextInfo{}
if replyTo != nil {
msgID, err := waid.ParseMessageID(replyTo.ID)
@ -64,7 +57,6 @@ func (mc *MessageConverter) generateContextInfo(
contextInfo.StanzaID = proto.String(msgID.ID)
contextInfo.Participant = proto.String(msgID.Sender.String())
contextInfo.QuotedMessage = &waE2E.Message{Conversation: proto.String("")}
contextInfo.QuotedType = waE2E.ContextInfo_EXPLICIT.Enum()
} else {
zerolog.Ctx(ctx).Warn().Err(err).
Stringer("reply_to_event_id", replyTo.MXID).
@ -85,9 +77,6 @@ func (mc *MessageConverter) generateContextInfo(
if setAt > 0 && contextInfo.Expiration != nil {
contextInfo.EphemeralSettingTimestamp = ptr.Ptr(setAt)
}
if roomMention {
contextInfo.NonJIDMentions = proto.Uint32(1)
}
return contextInfo
}
@ -107,7 +96,7 @@ func (mc *MessageConverter) ToWhatsApp(
}
message := &waE2E.Message{}
contextInfo := mc.generateContextInfo(ctx, replyTo, portal, content.BeeperDisappearingTimer, content.Mentions != nil && content.Mentions.Room)
contextInfo := mc.generateContextInfo(ctx, replyTo, portal, content.BeeperDisappearingTimer)
switch content.MsgType {
case event.MsgText, event.MsgNotice, event.MsgEmote:
@ -202,7 +191,6 @@ func (mc *MessageConverter) constructMediaMessage(
FileSHA256: uploaded.FileSHA256,
FileLength: proto.Uint64(uploaded.FileLength),
URL: proto.String(uploaded.URL),
IsLottie: proto.Bool(mime == "application/was"),
},
}
case event.MsgAudio:
@ -484,17 +472,6 @@ func (mc *MessageConverter) convertToWebP(img []byte) ([]byte, int, error) {
return webpBuffer.Bytes(), size, nil
}
func (mc *MessageConverter) getOriginalBridgedSticker(ctx context.Context, info *event.BridgedSticker) (*types.StickerPackItem, error) {
if info == nil || info.Network != StickerSourceID || !strings.HasPrefix(info.PackURL, StickerPackURLPrefix) || info.ID == "" {
return nil, nil
}
fileHash, err := base64.StdEncoding.DecodeString(info.ID)
if err != nil {
return nil, nil
}
return mc.GetCachedSticker(ctx, getClient(ctx), strings.TrimPrefix(info.PackURL, StickerPackURLPrefix), fileHash)
}
func (mc *MessageConverter) reuploadFileToWhatsApp(
ctx context.Context, content *event.MessageEventContent,
) (*whatsmeow.UploadResponse, []byte, string, error) {
@ -503,25 +480,7 @@ func (mc *MessageConverter) reuploadFileToWhatsApp(
if content.FileName != "" {
fileName = content.FileName
}
var data []byte
var err error
var sticker *types.StickerPackItem
if sticker, err = mc.getOriginalBridgedSticker(ctx, content.Info.BridgedSticker); err != nil {
zerolog.Ctx(ctx).Warn().Err(err).
Msg("Failed to get original bridged sticker, falling back to downloading from URL")
data, err = mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
} else if sticker != nil {
if sticker.MimeType == "application/was" {
data, err = getClient(ctx).Download(ctx, sticker)
mime = sticker.MimeType
} else {
data, err = mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
}
content.Info.Width = sticker.Width
content.Info.Height = sticker.Height
} else {
data, err = mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
}
data, err := mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
if err != nil {
return nil, nil, "", fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err)
}
@ -539,14 +498,7 @@ func (mc *MessageConverter) reuploadFileToWhatsApp(
case event.MessageType(event.EventSticker.Type):
isSticker = true
mediaType = whatsmeow.MediaImage
if mime == "video/lottie+json" {
// This likely won't work
data, err = PackAnimatedSticker(data)
if err != nil {
return nil, nil, mime, fmt.Errorf("%w (packing animated sticker): %w", bridgev2.ErrMediaConvertFailed, err)
}
mime = "application/was"
} else if (mime != "image/webp" || content.Info.Width != content.Info.Height) && mime != "application/was" {
if mime != "image/webp" || content.Info.Width != content.Info.Height {
var size int
data, size, err = mc.convertToWebP(data)
if err != nil {

View file

@ -140,9 +140,6 @@ func (mc *MessageConverter) ToMatrix(
isBackfill bool,
previouslyConvertedPart *bridgev2.ConvertedMessagePart,
) *bridgev2.ConvertedMessage {
if waMsg == nil {
waMsg = &waE2E.Message{}
}
ctx = context.WithValue(ctx, contextKeyClient, client)
ctx = context.WithValue(ctx, contextKeyIntent, intent)
ctx = context.WithValue(ctx, contextKeyPortal, portal)
@ -215,8 +212,6 @@ func (mc *MessageConverter) ToMatrix(
part, contextInfo = mc.convertPlaceholderMessage(ctx, waMsg)
case waMsg.GroupInviteMessage != nil:
part, contextInfo = mc.convertGroupInviteMessage(ctx, info, waMsg.GroupInviteMessage)
case waMsg.MessageHistoryNotice != nil:
part, contextInfo = mc.convertMessageHistoryNotice(ctx, info, waMsg.MessageHistoryNotice)
case waMsg.ProtocolMessage != nil && waMsg.ProtocolMessage.GetType() == waE2E.ProtocolMessage_EPHEMERAL_SETTING:
part, contextInfo = mc.convertEphemeralSettingMessage(ctx, waMsg.ProtocolMessage, info.Timestamp, isBackfill)
case waMsg.EncCommentMessage != nil:
@ -239,9 +234,6 @@ func (mc *MessageConverter) ToMatrix(
part.Extra["fi.mau.whatsapp.source_broadcast_list"] = info.Chat.String()
}
mc.addMentions(ctx, contextInfo.GetMentionedJID(), part.Content)
if contextInfo.GetNonJIDMentions() == 1 {
part.Content.Mentions.Room = true
}
cm := &bridgev2.ConvertedMessage{
Parts: []*bridgev2.ConvertedMessagePart{part},

View file

@ -71,7 +71,7 @@ func (mc *MessageConverter) PollStartToWhatsApp(
if maxAnswers >= len(content.PollStart.Answers) || maxAnswers < 0 {
maxAnswers = 0
}
contextInfo := mc.generateContextInfo(ctx, replyTo, portal, nil, content.Mentions != nil && content.Mentions.Room)
contextInfo := mc.generateContextInfo(ctx, replyTo, portal, nil)
var question string
question, contextInfo.MentionedJID = mc.msc1767ToWhatsApp(ctx, content.PollStart.Question, content.Mentions)
if len(question) == 0 {

View file

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

View file

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

View file

@ -20,7 +20,6 @@ import (
"bytes"
"context"
"fmt"
"html"
"image"
"math"
"net/http"
@ -52,7 +51,7 @@ func (mc *MessageConverter) convertLocationMessage(ctx context.Context, msg *waE
MsgType: event.MsgLocation,
Body: fmt.Sprintf("Location: %s\n%s\n%s", name, msg.GetAddress(), url),
Format: event.FormatHTML,
FormattedBody: fmt.Sprintf(`Location: <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()),
}

View file

@ -17,6 +17,8 @@
package msgconv
import (
"archive/zip"
"bytes"
"context"
"encoding/json"
"errors"
@ -24,19 +26,21 @@ import (
"io"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
"github.com/rs/zerolog"
"go.mau.fi/util/exfmt"
"go.mau.fi/util/exmime"
"go.mau.fi/util/exslices"
"go.mau.fi/util/lottie"
"go.mau.fi/util/random"
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/proto/waE2E"
"go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-whatsapp/pkg/waid"
)
@ -83,15 +87,15 @@ func (mc *MessageConverter) convertMediaMessage(
MimeType: msg.GetMimetype(),
}
if mc.DirectMedia {
preparedMedia.FillFileName()
if preparedMedia.Info.MimeType == "application/was" {
preparedMedia.Info.MimeType = "video/lottie+json"
preparedMedia.FileName = "sticker.json"
}
preparedMedia.FillFileName()
var err error
portal := getPortal(ctx)
idOverride := getEditTargetID(ctx)
preparedMedia.URL, err = portal.Bridge.Matrix.GenerateContentURI(ctx, waid.MakeMediaID(messageInfo, idOverride, portal.Receiver, getMediaIDVersion(msg)))
preparedMedia.URL, err = portal.Bridge.Matrix.GenerateContentURI(ctx, waid.MakeMediaID(messageInfo, idOverride, portal.Receiver))
if err != nil {
panic(fmt.Errorf("failed to generate content URI: %w", err))
}
@ -120,20 +124,13 @@ func (mc *MessageConverter) convertMediaMessage(
return
}
func getMediaIDVersion(msg MediaMessage) []byte {
if encSHA256 := msg.GetFileEncSHA256(); len(encSHA256) > 0 {
return encSHA256
}
return msg.GetFileSHA256()
}
func (mc *MessageConverter) convertAlbumMessage(ctx context.Context, msg *waE2E.AlbumMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
parts := make([]string, 0, 2)
if msg.GetExpectedImageCount() > 0 {
parts = append(parts, exfmt.Pluralizable("image")(int(msg.GetExpectedImageCount())))
parts = append(parts, fmt.Sprintf("%d images", msg.GetExpectedImageCount()))
}
if msg.GetExpectedVideoCount() > 0 {
parts = append(parts, exfmt.Pluralizable("video")(int(msg.GetExpectedVideoCount())))
parts = append(parts, fmt.Sprintf("%d videos", msg.GetExpectedVideoCount()))
}
var partDesc string
if len(parts) > 0 {
@ -201,9 +198,7 @@ type PreparedMedia struct {
}
func (pm *PreparedMedia) FillFileName() *PreparedMedia {
if pm.Type == event.EventSticker {
pm.FileName = ""
} else if pm.FileName == "" {
if pm.FileName == "" {
pm.FileName = strings.TrimPrefix(string(pm.MsgType), "m.") + exmime.ExtensionFromMimetype(pm.Info.MimeType)
}
return pm
@ -244,19 +239,6 @@ type MediaMessageWithDuration interface {
const WhatsAppStickerSize = 190
func fixStickerDimensions(info *event.FileInfo) {
if info.Width == info.Height {
info.Width = WhatsAppStickerSize
info.Height = WhatsAppStickerSize
} else if info.Width > info.Height {
info.Height /= info.Width / WhatsAppStickerSize
info.Width = WhatsAppStickerSize
} else {
info.Width /= info.Height / WhatsAppStickerSize
info.Height = WhatsAppStickerSize
}
}
func prepareMediaMessage(rawMsg MediaMessage) *PreparedMedia {
extraInfo := map[string]any{}
data := &PreparedMedia{
@ -305,7 +287,19 @@ func prepareMediaMessage(rawMsg MediaMessage) *PreparedMedia {
case *waE2E.StickerMessage:
data.Type = event.EventSticker
data.FileName = "sticker" + exmime.ExtensionFromMimetype(msg.GetMimetype())
fixStickerDimensions(data.Info)
if msg.GetMimetype() == "application/was" && data.FileName == "sticker" {
data.FileName = "sticker.json"
}
if data.Info.Width == data.Info.Height {
data.Info.Width = WhatsAppStickerSize
data.Info.Height = WhatsAppStickerSize
} else if data.Info.Width > data.Info.Height {
data.Info.Height /= data.Info.Width / WhatsAppStickerSize
data.Info.Width = WhatsAppStickerSize
} else {
data.Info.Width /= data.Info.Height / WhatsAppStickerSize
data.Info.Height = WhatsAppStickerSize
}
case *waE2E.VideoMessage:
data.MsgType = event.MsgVideo
pairedMediaType := msg.GetContextInfo().GetPairedMediaType()
@ -365,17 +359,14 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
) error {
client := getClient(ctx)
intent := getIntent(ctx)
var roomID id.RoomID
if portal := getPortal(ctx); portal != nil {
roomID = portal.MXID
}
portal := getPortal(ctx)
var thumbnailData []byte
var thumbnailInfo *event.FileInfo
if part.Info.Size > uploadFileThreshold {
var err error
part.URL, part.File, err = intent.UploadMediaStream(ctx, roomID, -1, true, func(file io.Writer) (*bridgev2.FileStreamResult, error) {
part.URL, part.File, err = intent.UploadMediaStream(ctx, portal.MXID, -1, true, func(file io.Writer) (*bridgev2.FileStreamResult, error) {
err := client.DownloadToFile(ctx, message, file.(*os.File))
if errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
if errors.Is(err, whatsmeow.ErrFileLengthMismatch) || errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
zerolog.Ctx(ctx).Warn().Err(err).Msg("Mismatching media checksums in message. Ignoring because WhatsApp seems to ignore them too")
} else if err != nil {
return nil, fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err)
@ -396,7 +387,7 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
}
} else {
data, err := client.Download(ctx, message)
if errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
if errors.Is(err, whatsmeow.ErrFileLengthMismatch) || errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
zerolog.Ctx(ctx).Warn().Err(err).Msg("Mismatching media checksums in message. Ignoring because WhatsApp seems to ignore them too")
} else if err != nil {
return fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err)
@ -406,14 +397,12 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
if err != nil {
return err
}
} else if part.Type == event.EventSticker && part.Info.MimeType == "image/webp" {
mc.fillWebPStickerInfo(ctx, part, data)
}
if part.Info.MimeType == "" {
part.Info.MimeType = http.DetectContentType(data)
}
part.FillFileName()
part.URL, part.File, err = intent.UploadMedia(ctx, roomID, data, part.FileName, part.Info.MimeType)
part.URL, part.File, err = intent.UploadMedia(ctx, portal.MXID, data, part.FileName, part.Info.MimeType)
if err != nil {
return fmt.Errorf("%w: %w", bridgev2.ErrMediaReuploadFailed, err)
}
@ -422,7 +411,7 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
var err error
part.Info.ThumbnailURL, part.Info.ThumbnailFile, err = intent.UploadMedia(
ctx,
roomID,
portal.MXID,
thumbnailData,
"thumbnail"+exmime.ExtensionFromMimetype(thumbnailInfo.MimeType),
thumbnailInfo.MimeType,
@ -436,6 +425,68 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
return nil
}
func (mc *MessageConverter) extractAnimatedSticker(fileInfo *PreparedMedia, data []byte) ([]byte, error) {
data, err := ExtractAnimatedSticker(data)
if err != nil {
return nil, err
}
fileInfo.Info.MimeType = "video/lottie+json"
fileInfo.FileName = "sticker.json"
return data, nil
}
func (mc *MessageConverter) convertAnimatedSticker(ctx context.Context, fileInfo *PreparedMedia, data []byte) ([]byte, []byte, *event.FileInfo, error) {
data, err := mc.extractAnimatedSticker(fileInfo, data)
if err != nil {
return nil, nil, nil, err
}
c := mc.AnimatedStickerConfig
if c.Target == "disable" {
return data, nil, nil, nil
} else if !lottie.Supported() {
zerolog.Ctx(ctx).Warn().Msg("Animated sticker conversion is enabled, but lottieconverter is not installed")
return data, nil, nil, nil
}
input := bytes.NewReader(data)
fileInfo.Info.MimeType = "image/" + c.Target
fileInfo.FileName = "sticker." + c.Target
switch c.Target {
case "png":
var output bytes.Buffer
err = lottie.Convert(ctx, input, "", &output, c.Target, c.Args.Width, c.Args.Height, "1")
return output.Bytes(), nil, nil, err
case "gif":
var output bytes.Buffer
err = lottie.Convert(ctx, input, "", &output, c.Target, c.Args.Width, c.Args.Height, strconv.Itoa(c.Args.FPS))
return output.Bytes(), nil, nil, err
case "webm", "webp":
tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("mautrix-whatsapp-lottieconverter-%s.%s", random.String(10), c.Target))
defer func() {
_ = os.Remove(tmpFile)
}()
thumbnailData, err := lottie.FFmpegConvert(ctx, input, tmpFile, c.Args.Width, c.Args.Height, c.Args.FPS)
if err != nil {
return nil, nil, nil, err
}
data, err = os.ReadFile(tmpFile)
if err != nil {
return nil, nil, nil, fmt.Errorf("failed to read converted file: %w", err)
}
var thumbnailInfo *event.FileInfo
if thumbnailData != nil {
thumbnailInfo = &event.FileInfo{
MimeType: "image/png",
Width: c.Args.Width,
Height: c.Args.Height,
Size: len(thumbnailData),
}
}
return data, thumbnailData, thumbnailInfo, nil
default:
return nil, nil, nil, fmt.Errorf("unsupported target format %s", c.Target)
}
}
func (mc *MessageConverter) makeMediaFailure(ctx context.Context, mediaInfo *PreparedMedia, keys *FailedMediaKeys, err error) *bridgev2.ConvertedMessagePart {
logLevel := zerolog.ErrorLevel
var extra map[string]any
@ -480,3 +531,28 @@ func (mc *MessageConverter) makeMediaFailure(ctx context.Context, mediaInfo *Pre
}
return part
}
func ExtractAnimatedSticker(data []byte) ([]byte, error) {
zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
if err != nil {
return nil, fmt.Errorf("failed to read sticker zip: %w", err)
}
animationFile, err := zipReader.Open("animation/animation.json")
if err != nil {
return nil, fmt.Errorf("failed to open animation.json: %w", err)
}
animationFileInfo, err := animationFile.Stat()
if err != nil {
_ = animationFile.Close()
return nil, fmt.Errorf("failed to stat animation.json: %w", err)
} else if animationFileInfo.Size() > uploadFileThreshold {
_ = animationFile.Close()
return nil, fmt.Errorf("animation.json is too large (%.2f MiB)", float64(animationFileInfo.Size())/1024/1024)
}
data, err = io.ReadAll(animationFile)
_ = animationFile.Close()
if err != nil {
return nil, fmt.Errorf("failed to read animation.json: %w", err)
}
return data, nil
}

View file

@ -20,16 +20,14 @@ import (
"context"
"encoding/base64"
"fmt"
"html"
"html/template"
"strings"
"time"
"github.com/rs/zerolog"
"go.mau.fi/util/exerrors"
"go.mau.fi/util/exfmt"
"go.mau.fi/util/ptr"
"go.mau.fi/whatsmeow/proto/waAICommonDeprecated"
"go.mau.fi/whatsmeow/proto/waAICommon"
"go.mau.fi/whatsmeow/proto/waE2E"
"go.mau.fi/whatsmeow/types"
"google.golang.org/protobuf/proto"
@ -102,7 +100,7 @@ func (mc *MessageConverter) convertGroupInviteMessage(ctx context.Context, info
}
}
htmlMessage := fmt.Sprintf(template, event.TextToHTML(msg.GetCaption()), html.EscapeString(msg.GetGroupName()), expiry, mc.Bridge.Config.CommandPrefix)
htmlMessage := fmt.Sprintf(template, event.TextToHTML(msg.GetCaption()), msg.GetGroupName(), expiry, mc.Bridge.Config.CommandPrefix)
content := &event.MessageEventContent{
MsgType: event.MsgText,
Body: format.HTMLToText(htmlMessage),
@ -119,92 +117,6 @@ func (mc *MessageConverter) convertGroupInviteMessage(ctx context.Context, info
}, msg.GetContextInfo()
}
func (mc *MessageConverter) formatMessageHistoryNoticeJID(ctx context.Context, jid types.JID) string {
_, displayName, err := mc.getBasicUserInfo(ctx, jid)
if err != nil {
zerolog.Ctx(ctx).Err(err).Stringer("jid", jid).Msg("Failed to get user info for message history notice")
} else if displayName != "" {
return displayName
}
switch jid.Server {
case types.DefaultUserServer:
return "+" + jid.User
default:
return "Unknown user " + jid.String()
}
}
const maxMessageHistoryNoticeReceivers = 5
var others = exfmt.Pluralizable("other")
func (mc *MessageConverter) formatMessageHistoryNoticeReceivers(ctx context.Context, receivers []string) string {
receiverLimit := min(len(receivers), maxMessageHistoryNoticeReceivers)
receiverNames := make([]string, 0, receiverLimit)
for _, receiver := range receivers[:receiverLimit] {
jid, err := types.ParseJID(receiver)
if err != nil {
zerolog.Ctx(ctx).Err(err).Str("receiver", receiver).Msg("Failed to parse message history receiver JID")
receiverNames = append(receiverNames, receiver)
} else {
receiverNames = append(receiverNames, mc.formatMessageHistoryNoticeJID(ctx, jid))
}
}
receiverText := strings.Join(receiverNames, ", ")
if len(receivers) > receiverLimit {
receiverText = fmt.Sprintf("%s + %s", receiverText, others(len(receivers)-receiverLimit))
}
return receiverText
}
func (mc *MessageConverter) messageHistoryNoticeLocation(ctx context.Context) *time.Location {
portal := getPortal(ctx)
loginID := portal.Receiver
if loginID == "" {
loginID = waid.MakeUserLoginID(getClient(ctx).Store.GetJID().ToNonAD())
}
if login := mc.Bridge.GetCachedUserLoginByID(loginID); login != nil {
meta, _ := login.Metadata.(*waid.UserLoginMetadata)
loc, err := meta.LoadTimezone()
if err != nil {
zerolog.Ctx(ctx).Err(err).Str("timezone", meta.Timezone).Msg("Failed to load user timezone for message history notice")
} else if loc != nil {
return loc
}
}
return time.Local
}
func (mc *MessageConverter) convertMessageHistoryNotice(ctx context.Context, info *types.MessageInfo, msg *waE2E.MessageHistoryNotice) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
metadata := msg.GetMessageHistoryMetadata()
sender := mc.formatMessageHistoryNoticeJID(ctx, info.Sender)
body := fmt.Sprintf("%s sent message history", sender)
if receiverText := mc.formatMessageHistoryNoticeReceivers(ctx, metadata.GetHistoryReceivers()); receiverText != "" {
body = fmt.Sprintf("%s to %s", body, receiverText)
}
if count := metadata.GetMessageCount(); count > 0 {
messageWord := "messages"
if count == 1 {
messageWord = "message"
}
body = fmt.Sprintf("%s (%d %s)", body, count, messageWord)
}
if metadata != nil && metadata.OldestMessageTimestampInWindow != nil {
oldestTS := time.Unix(metadata.GetOldestMessageTimestampInWindow(), 0).In(mc.messageHistoryNoticeLocation(ctx))
body = fmt.Sprintf("%s, starting %s", body, oldestTS.Format("Jan 2, 2006 at 3:04 PM"))
}
body += "."
return &bridgev2.ConvertedMessagePart{
Type: event.EventMessage,
Content: &event.MessageEventContent{
MsgType: event.MsgNotice,
Body: body,
},
}, msg.GetContextInfo()
}
func (mc *MessageConverter) convertEphemeralSettingMessage(ctx context.Context, msg *waE2E.ProtocolMessage, ts time.Time, isBackfill bool) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
portal := getPortal(ctx)
portalMeta := portal.Metadata.(*waid.PortalMetadata)
@ -354,9 +266,8 @@ func (mc *MessageConverter) convertKeepInChatMessage(ctx context.Context, msg *w
func (mc *MessageConverter) convertRichResponseMessage(ctx context.Context, msg *waE2E.AIRichResponseMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
var body strings.Builder
// TODO switch to new format?
for i, submsg := range msg.GetSubmessages() {
if submsg.GetMessageType() == waAICommonDeprecated.AIRichResponseSubMessageType_AI_RICH_RESPONSE_TEXT {
if submsg.GetMessageType() == waAICommon.AIRichResponseSubMessageType_AI_RICH_RESPONSE_TEXT {
if i > 0 {
body.WriteString("\n")
}

View file

@ -177,9 +177,6 @@ func (mc *MessageConverter) convertPollUpdateMessage(ctx context.Context, info *
if err != nil {
log.Err(err).Msg("Failed to get poll update target message")
return failedPollUpdatePart, nil
} else if pollMessage == nil {
log.Warn().Str("target_message_id", string(pollMessageID)).Msg("Poll update target message not found")
return failedPollUpdatePart, nil
}
vote, err := getClient(ctx).DecryptPollVote(ctx, &events.Message{
Info: *info,

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"`
HistorySyncPortalsNeedCreating bool `json:"history_sync_portals_need_creating,omitempty"`
ReachoutTimelockUntil time.Time `json:"reachout_timelock_until,omitempty"`
HistorySyncPortalsNeedCreating bool `json:"history_sync_portals_need_creating,omitempty"`
MData json.RawMessage `json:"mdata,omitempty"`
}
@ -62,13 +61,6 @@ func (m *UserLoginMetadata) GeneratePushKeys() {
}
}
func (m *UserLoginMetadata) LoadTimezone() (*time.Location, error) {
if m == nil || m.Timezone == "" {
return nil, nil
}
return time.LoadLocation(m.Timezone)
}
type MessageErrorType string
const (

View file

@ -88,10 +88,9 @@ func MakeFakeMessageID(chat, sender types.JID, data string) networkid.MessageID
}
type ParsedMessageID struct {
Chat types.JID
Sender types.JID
ID types.MessageID
Version []byte
Chat types.JID
Sender types.JID
ID types.MessageID
}
func (pmi *ParsedMessageID) String() networkid.MessageID {

View file

@ -33,10 +33,9 @@ const (
mediaIDTypeMessage = 255
mediaIDTypeAvatar = 254
mediaIDTypeCommunityAvatar = 253
mediaIDTypeStickerPackItem = 252
)
func MakeMediaID(messageInfo *types.MessageInfo, idOverride types.MessageID, receiver networkid.UserLoginID, version []byte) networkid.MediaID {
func MakeMediaID(messageInfo *types.MessageInfo, idOverride types.MessageID, receiver networkid.UserLoginID) networkid.MediaID {
compactChat := compactJID(messageInfo.Chat.ToNonAD())
compactSender := compactJID(messageInfo.Sender.ToNonAD())
receiverID := compactJID(ParseUserLoginID(receiver, 0))
@ -46,7 +45,7 @@ func MakeMediaID(messageInfo *types.MessageInfo, idOverride types.MessageID, rec
} else {
compactID = compactMsgID(messageInfo.ID)
}
mediaID := make([]byte, 0, 6+len(compactChat)+len(compactSender)+len(receiverID)+len(compactID)+len(version))
mediaID := make([]byte, 0, 5+len(compactChat)+len(compactSender)+len(receiverID)+len(compactID))
mediaID = append(mediaID, mediaIDTypeMessage)
mediaID = append(mediaID, byte(len(compactChat)))
mediaID = append(mediaID, compactChat...)
@ -56,8 +55,6 @@ func MakeMediaID(messageInfo *types.MessageInfo, idOverride types.MessageID, rec
mediaID = append(mediaID, receiverID...)
mediaID = append(mediaID, byte(len(compactID)))
mediaID = append(mediaID, compactID...)
mediaID = append(mediaID, byte(len(version)))
mediaID = append(mediaID, version...)
return mediaID
}
@ -85,28 +82,9 @@ type AvatarMediaInfo struct {
Community bool
}
func MakeStickerPackMediaID(packID string, fileHash []byte, receiver networkid.UserLoginID) networkid.MediaID {
receiverID := compactJID(ParseUserLoginID(receiver, 0))
mediaID := make([]byte, 0, 4+len(packID)+len(fileHash)+len(receiverID))
mediaID = append(mediaID, mediaIDTypeStickerPackItem)
mediaID = append(mediaID, byte(len(packID)))
mediaID = append(mediaID, packID...)
mediaID = append(mediaID, byte(len(fileHash)))
mediaID = append(mediaID, fileHash...)
mediaID = append(mediaID, byte(len(receiverID)))
mediaID = append(mediaID, receiverID...)
return mediaID
}
type StickerPackMediaInfo struct {
PackID string
FileHash []byte
}
type ParsedMediaID struct {
Message *ParsedMessageID
Avatar *AvatarMediaInfo
Sticker *StickerPackMediaInfo
UserLogin networkid.UserLoginID
}
@ -140,12 +118,6 @@ func ParseMediaID(mediaID networkid.MediaID) (*ParsedMediaID, error) {
Sender: senderJID,
ID: id,
}
if len(mediaID) > 0 {
parsed.Message.Version, err = readCompact(&mediaID, rawBytes)
if err != nil {
return nil, fmt.Errorf("failed to parse version: %w", err)
}
}
parsed.UserLogin = MakeUserLoginID(receiverID)
case mediaIDTypeAvatar, mediaIDTypeCommunityAvatar:
targetJID, err := readCompact(&mediaID, parseCompactJID)
@ -166,24 +138,6 @@ func ParseMediaID(mediaID networkid.MediaID) (*ParsedMediaID, error) {
Community: mediaIDType == mediaIDTypeCommunityAvatar,
}
parsed.UserLogin = MakeUserLoginID(receiverID)
case mediaIDTypeStickerPackItem:
packID, err := readCompact(&mediaID, parseString)
if err != nil {
return nil, fmt.Errorf("failed to parse sticker pack ID: %w", err)
}
fileHash, err := readCompact(&mediaID, rawBytes)
if err != nil {
return nil, fmt.Errorf("failed to parse sticker file hash: %w", err)
}
receiverID, err := readCompact(&mediaID, parseCompactJID)
if err != nil {
return nil, fmt.Errorf("failed to parse receiver JID: %w", err)
}
parsed.Sticker = &StickerPackMediaInfo{
PackID: packID,
FileHash: fileHash,
}
parsed.UserLogin = MakeUserLoginID(receiverID)
default:
return nil, fmt.Errorf("unknown media ID type %d", mediaIDType)
}
@ -292,10 +246,6 @@ func parseCompactJID(jid []byte) (types.JID, error) {
}
}
func rawBytes(data []byte) ([]byte, error) {
return data, nil
}
func readCompact[T any](data *networkid.MediaID, fn func(data []byte) (T, error)) (T, error) {
var defVal T
if len(*data) < 1 {