Watch
1
0
Fork
You've already forked mautrix-whatsapp
0
mirror of https://github.com/mautrix/whatsapp.git synced 2026-11-10 08:09:19 -05:00

Compare commits

..
Author SHA1 Message Date
Kishan Bagaria
5a7217e0ff - 2026-03-27 09:10:36 -07:00
Kishan Bagaria
7ee5aa647c - 2026-03-26 22:02:48 -07:00
60 changed files with 1230 additions and 4158 deletions

View file

@ -1 +0,0 @@
code_format_allowlist: ["wa-connect-failure-%d"]

View file

@ -7,12 +7,10 @@ type: Bug
--- ---
<!-- Include relevant logs, the bridge version and other important details here --> <!--
Remember to include relevant logs, the bridge version and any other details.
### Checklist It's always best to ask in the Matrix room first, especially if you aren't sure
what details are needed. Issues with insufficient detail will likely just be
<!-- All items below are mandatory. Issues not following the rules may be closed without comment. --> ignored or closed immediately.
-->
* [ ] This is an actual bug, not just a setup issue (see the [troubleshooting docs](https://docs.mau.fi/bridges/general/troubleshooting.html) or ask in the Matrix room for setup help).
* [ ] I am certain that sufficient information is included. Ask in the Matrix room first if not.
* [ ] The bug is still present on the main branch. The `!wa version` command output is: ``

View file

@ -11,14 +11,14 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
go-version: ["1.26", "1.27"] go-version: ["1.25", "1.26"]
name: Lint ${{ matrix.go-version == '1.27' && '(latest)' || '(old)' }} name: Lint ${{ matrix.go-version == '1.26' && '(latest)' || '(old)' }}
steps: steps:
- uses: actions/checkout@v7 - uses: actions/checkout@v6
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v7 uses: actions/setup-go@v6
with: with:
go-version: ${{ matrix.go-version }} go-version: ${{ matrix.go-version }}
cache: true cache: true
@ -30,7 +30,6 @@ jobs:
run: | run: |
go install golang.org/x/tools/cmd/goimports@latest go install golang.org/x/tools/cmd/goimports@latest
go install honnef.co/go/tools/cmd/staticcheck@latest go install honnef.co/go/tools/cmd/staticcheck@latest
go install github.com/beeper/pre-commit-go/bridgelint@latest
export PATH="$HOME/go/bin:$PATH" export PATH="$HOME/go/bin:$PATH"
- name: Run pre-commit - name: Run pre-commit

View file

@ -21,12 +21,9 @@ repos:
- id: go-mod-tidy - id: go-mod-tidy
- repo: https://github.com/beeper/pre-commit-go - repo: https://github.com/beeper/pre-commit-go
rev: v0.5.0 rev: v0.4.2
hooks: hooks:
- id: zerolog-ban-msgf - id: zerolog-ban-msgf
- id: zerolog-use-stringer - id: zerolog-use-stringer
- id: prevent-literal-http-methods - id: prevent-literal-http-methods
- id: zerolog-ban-global-log - id: zerolog-ban-global-log
- id: bridgelint
language: system
verbose: true

View file

@ -1,77 +1,3 @@
# v26.10
* Added support for new rich response messages used by the Muse AI bot.
* Added support for bridging custom profile set for Muse.
* Added automatic one-time resync of keys needed to decrypt Muse messages.
* Added support for blocking and unblocking users on WhatsApp.
* Added support for contact array messages (thanks to [@voska] in [#961]).
* Added partial support for filling quoted message when sending replies to
WhatsApp.
* Changed undecryptable message resends to only bridge as edits and never as new
messages, to avoid races if the message was deleted before the resend.
* Changed login to wait for OTKs to be successfully uploaded, as the login will
fail if the bridge is stopped before that point.
* Changed chat info syncing to not bridge mute status if app state sync is
failing.
* Changed power levels to allow poll votes in read-only groups even if the room
is encrypted.
* Fixed backfilled media retries not getting sent if configured for local time.
* Fixed "delete for me" from WhatsApp not being bridged correctly in DMs.
* Fixed very long WhatsApp messages not being bridged.
* Fixed document message caption edits not being handled correctly.
[@voska]: https://github.com/voska
[#961]: https://github.com/mautrix/whatsapp/pull/961
# v26.09
* Bumped minimum Go version to 1.26.
* Added option to limit eager member sync in group chat portals.
* Added option to fetch avatars lazily when using direct media.
* Added one-time migration to ensure LID ghost avatars are consistent with the
old phone number ghosts.
* Fixed the resolve identifier command and API not returning any user info for
users who hadn't previously been encountered.
* Fixed group portal power levels to always allow poll responses.
* Fixed handling history sync events where the phone sends nonsensical
timestamps.
* Fixed handling edits to HD media captions.
* Fixed handling bridging own read receipts from the native apps in channels.
* Fixed decrypting messages from the new Muse AI bot
* May require resyncing app state using `!wa sync appstate regular_high`.
* The message contents aren't supported yet.
# v26.08
* Switched direct chats to use LIDs instead of phone numbers.
# v26.07
* Updated Docker image to Alpine 3.24.
* Added support for the extra passkey authentication step when logging in.
* Added support for newer versions of poll messages from WhatsApp.
* Fixed poll votes in DMs.
# v26.06
* Added placeholder for group message history share notices.
* Fixed community spaces not being bridged properly in some cases.
* Fixed handling edits of HD media.
* Fixed duplicate message when a message from another own device is initially
undecryptable and later gets resolved.
# v26.05
* Added support for importing sticker packs from WhatsApp.
* Added support for WhatsApp's new message edit encryption scheme.
# v26.04
* Added support for @room mentions in both directions.
* Changed initial backfill to happen even if WhatsApp doesn't send full history.
* Fixed panic when handling updates to unknown polls from WhatsApp.
* Fixed some background loops not stopping when a user is logged out.
# v26.03 # v26.03
* Added option to save outgoing messages in the database to allow encryption * Added option to save outgoing messages in the database to allow encryption

View file

@ -1 +0,0 @@
See <https://docs.mau.fi/bridges/general/contributing.html>

View file

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

View file

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

View file

@ -0,0 +1,153 @@
package main
import (
"net/http"
"strings"
"github.com/rs/zerolog/hlog"
"go.mau.fi/util/exhttp"
"go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/appstate"
"go.mau.fi/whatsmeow/types"
"maunium.net/go/mautrix"
"maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/matrix"
"maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-whatsapp/pkg/connector"
"go.mau.fi/mautrix-whatsapp/pkg/waid"
)
type OtherUserInfo struct {
MXID id.UserID `json:"mxid"`
JID types.JID `json:"jid"`
Name string `json:"displayname"`
Avatar id.ContentURIString `json:"avatar_url"`
}
type PortalInfo struct {
RoomID id.RoomID `json:"room_id"`
OtherUser *OtherUserInfo `json:"other_user,omitempty"`
GroupInfo *types.GroupInfo `json:"group_info,omitempty"`
JustCreated bool `json:"just_created"`
}
type Error struct {
Success bool `json:"success"`
Error string `json:"error"`
ErrCode string `json:"errcode"`
}
func legacyProvContacts(w http.ResponseWriter, r *http.Request) {
userLogin := m.Matrix.Provisioning.GetLoginForRequest(w, r)
if userLogin == nil {
return
}
if contacts, err := userLogin.Client.(*connector.WhatsAppClient).GetStore().Contacts.GetAllContacts(r.Context()); err != nil {
hlog.FromRequest(r).Err(err).Msg("Failed to fetch all contacts")
exhttp.WriteJSONResponse(w, http.StatusInternalServerError, Error{
Error: "Internal server error while fetching contact list",
ErrCode: "failed to get contacts",
})
} else {
augmentedContacts := map[types.JID]any{}
for jid, contact := range contacts {
var avatarURL id.ContentURIString
if puppet, _ := m.Bridge.GetExistingGhostByID(r.Context(), waid.MakeUserID(jid)); puppet != nil {
avatarURL = puppet.AvatarMXC
}
augmentedContacts[jid] = map[string]interface{}{
"Found": contact.Found,
"FirstName": contact.FirstName,
"FullName": contact.FullName,
"PushName": contact.PushName,
"BusinessName": contact.BusinessName,
"AvatarURL": avatarURL,
}
}
exhttp.WriteJSONResponse(w, http.StatusOK, augmentedContacts)
}
}
func legacyProvResolveIdentifier(w http.ResponseWriter, r *http.Request) {
number := r.PathValue("number")
userLogin := m.Matrix.Provisioning.GetLoginForRequest(w, r)
if userLogin == nil {
return
}
startChat := strings.Contains(r.URL.Path, "/v1/pm/")
resp, err := userLogin.Client.(*connector.WhatsAppClient).ResolveIdentifier(r.Context(), number, startChat)
if err != nil {
hlog.FromRequest(r).Warn().Err(err).Str("identifier", number).Msg("Failed to resolve identifier")
matrix.RespondWithError(w, err, "Internal error resolving identifier")
return
}
var portal *bridgev2.Portal
if startChat {
portal, err = m.Bridge.GetPortalByKey(r.Context(), resp.Chat.PortalKey)
if err != nil {
hlog.FromRequest(r).Warn().Err(err).Stringer("portal_key", resp.Chat.PortalKey).Msg("Failed to get portal by key")
matrix.RespondWithError(w, err, "Internal error getting portal by key")
return
}
err = portal.CreateMatrixRoom(r.Context(), userLogin, nil)
if err != nil {
hlog.FromRequest(r).Warn().Err(err).Stringer("portal_key", resp.Chat.PortalKey).Msg("Failed to create matrix room for portal")
matrix.RespondWithError(w, err, "Internal error creating matrix room for portal")
return
}
} else {
portal, _ = m.Bridge.GetExistingPortalByKey(r.Context(), resp.Chat.PortalKey)
}
var roomID id.RoomID
if portal != nil {
roomID = portal.MXID
}
exhttp.WriteJSONResponse(w, http.StatusOK, PortalInfo{
RoomID: roomID,
OtherUser: &OtherUserInfo{
JID: waid.ParseUserID(resp.UserID),
MXID: resp.Ghost.Intent.GetMXID(),
Name: resp.Ghost.Name,
Avatar: resp.Ghost.AvatarMXC,
},
})
}
func provAppStateDebug(w http.ResponseWriter, r *http.Request) {
userLogin := m.Matrix.Provisioning.GetLoginForRequest(w, r)
if userLogin == nil {
return
}
client := userLogin.Client.(*connector.WhatsAppClient)
if client.Client == nil {
mautrix.MNotFound.WithMessage("WhatsApp client not connected").Write(w)
return
}
client.Client.AppStateDebugLogs = true
err := client.Client.FetchAppState(r.Context(), appstate.WAPatchName(r.PathValue("patch")), r.URL.Query().Get("full") == "1", false)
client.Client.AppStateDebugLogs = false
if err != nil {
mautrix.MUnknown.WithMessage("Failed to fetch app state: %v", err).Write(w)
} else {
exhttp.WriteEmptyJSONResponse(w, http.StatusOK)
}
}
func provRecoverAppStateDebug(w http.ResponseWriter, r *http.Request) {
userLogin := m.Matrix.Provisioning.GetLoginForRequest(w, r)
if userLogin == nil {
return
}
client := userLogin.Client.(*connector.WhatsAppClient)
if client.Client == nil {
mautrix.MNotFound.WithMessage("WhatsApp client not connected").Write(w)
return
}
resp, err := client.Client.SendPeerMessage(r.Context(), whatsmeow.BuildAppStateRecoveryRequest(appstate.WAPatchName(r.PathValue("patch"))))
if err != nil {
mautrix.MUnknown.WithMessage("Failed to send app state recovery request: %v", err).Write(w)
} else {
exhttp.WriteJSONResponse(w, http.StatusOK, resp)
}
}

View file

@ -18,12 +18,21 @@ var m = mxmain.BridgeMain{
Name: "mautrix-whatsapp", Name: "mautrix-whatsapp",
URL: "https://github.com/mautrix/whatsapp", URL: "https://github.com/mautrix/whatsapp",
Description: "A Matrix-WhatsApp puppeting bridge.", Description: "A Matrix-WhatsApp puppeting bridge.",
Version: "26.10", Version: "26.03",
SemCalVer: true, SemCalVer: true,
Connector: &connector.WhatsAppConnector{}, Connector: &connector.WhatsAppConnector{},
} }
func main() { func main() {
m.PostStart = func() {
if m.Matrix.Provisioning != nil {
m.Matrix.Provisioning.Router.HandleFunc("GET /v1/contacts", legacyProvContacts)
m.Matrix.Provisioning.Router.HandleFunc("GET /v1/resolve_identifier/{number}", legacyProvResolveIdentifier)
m.Matrix.Provisioning.Router.HandleFunc("POST /v1/pm/{number}", legacyProvResolveIdentifier)
m.Matrix.Provisioning.Router.HandleFunc("POST /v1/debug/appstate/{patch}", provAppStateDebug)
m.Matrix.Provisioning.Router.HandleFunc("POST /v1/debug/recover-appstate/{patch}", provRecoverAppStateDebug)
}
}
m.InitVersion(Tag, Commit, BuildTime) m.InitVersion(Tag, Commit, BuildTime)
m.Run() m.Run()
} }

View file

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

50
go.mod
View file

@ -1,53 +1,53 @@
module go.mau.fi/mautrix-whatsapp module go.mau.fi/mautrix-whatsapp
go 1.26.0 go 1.25.0
toolchain go1.27.2 toolchain go1.26.1
tool go.mau.fi/util/cmd/maubuild tool go.mau.fi/util/cmd/maubuild
require ( require (
github.com/google/uuid v1.6.0 github.com/lib/pq v1.11.2
github.com/lib/pq v1.12.3 github.com/rs/zerolog v1.34.0
github.com/rs/zerolog v1.35.1 go.mau.fi/util v0.9.7
github.com/tidwall/gjson v1.20.0 go.mau.fi/webp v0.2.0
github.com/yuin/goldmark v1.8.6 go.mau.fi/whatsmeow v0.0.0-20260305215846-fc65416c22c4
go.mau.fi/util v0.11.0 golang.org/x/image v0.37.0
go.mau.fi/webp v0.3.0 golang.org/x/net v0.52.0
go.mau.fi/whatsmeow v0.0.0-20261026125351-1fbae988179a golang.org/x/sync v0.20.0
golang.org/x/image v0.47.0 google.golang.org/protobuf v1.36.11
golang.org/x/net v0.61.0
golang.org/x/sync v0.24.0
google.golang.org/protobuf v1.36.12
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
maunium.net/go/mautrix v0.32.1-0.20261105141340-842796acf445 maunium.net/go/mautrix v0.26.5-0.20260322102453-0c955c396df7
) )
require ( require (
filippo.io/edwards25519 v1.2.0 // indirect filippo.io/edwards25519 v1.2.0 // indirect
github.com/beeper/argo-go v1.1.2 // indirect github.com/beeper/argo-go v1.1.2 // indirect
github.com/coder/websocket v1.8.15 // indirect github.com/coder/websocket v1.8.14 // indirect
github.com/coreos/go-systemd/v22 v22.7.0 // indirect github.com/coreos/go-systemd/v22 v22.6.0 // indirect
github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/kr/pretty v0.3.1 // indirect github.com/kr/pretty v0.3.1 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v1.14.52 // indirect github.com/mattn/go-sqlite3 v1.14.34 // indirect
github.com/petermattis/goid v0.0.0-20260918085751-abfca077860b // indirect github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/rs/xid v1.6.0 // indirect github.com/rs/xid v1.6.0 // indirect
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
github.com/tidwall/gjson v1.18.0 // indirect
github.com/tidwall/match v1.2.0 // indirect github.com/tidwall/match v1.2.0 // indirect
github.com/tidwall/pretty v1.2.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect
github.com/tidwall/sjson v1.2.5 // indirect github.com/tidwall/sjson v1.2.5 // indirect
github.com/vektah/gqlparser/v2 v2.5.27 // indirect github.com/vektah/gqlparser/v2 v2.5.27 // indirect
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 go.mau.fi/zeroconfig v0.2.0 // indirect
golang.org/x/crypto v0.58.0 // indirect golang.org/x/crypto v0.49.0 // indirect
golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e // indirect golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
golang.org/x/mod v0.42.0 // indirect golang.org/x/mod v0.34.0 // indirect
golang.org/x/sys v0.49.0 // indirect golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.43.0 // indirect golang.org/x/text v0.35.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
maunium.net/go/mauflag v1.0.0 // indirect maunium.net/go/mauflag v1.0.0 // indirect

104
go.sum
View file

@ -8,13 +8,17 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNg
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
github.com/beeper/argo-go v1.1.2 h1:UQI2G8F+NLfGTOmTUI0254pGKx/HUU/etbUGTJv91Fs= github.com/beeper/argo-go v1.1.2 h1:UQI2G8F+NLfGTOmTUI0254pGKx/HUU/etbUGTJv91Fs=
github.com/beeper/argo-go v1.1.2/go.mod h1:M+LJAnyowKVQ6Rdj6XYGEn+qcVFkb3R/MUpqkGR0hM4= github.com/beeper/argo-go v1.1.2/go.mod h1:M+LJAnyowKVQ6Rdj6XYGEn+qcVFkb3R/MUpqkGR0hM4=
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA= github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg= github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w= github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg= github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo= github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
@ -26,33 +30,39 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ= github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA= github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mattn/go-sqlite3 v1.14.52 h1:wVbm2Qnf4OXkqhBTSPuCRZDRnxfbVrrmiCEroVdog8U= github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
github.com/mattn/go-sqlite3 v1.14.52/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w= github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
github.com/petermattis/goid v0.0.0-20260918085751-abfca077860b h1:OzNsuVdSWGwXvWKTtChx9ve89k4cFJ6NxYM4Aw4/f+E= github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6 h1:rh2lKw/P/EqHa724vYH2+VVQ1YnW4u6EOXl0PMAovZE=
github.com/petermattis/goid v0.0.0-20260918085751-abfca077860b/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4= github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU= github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0= github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI= github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw= github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stretchr/testify v1.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/gjson v1.20.0 h1:+agJ3rEzKcCXKDKo0ml26UROcpcAlnZyjq+TjbY5Fto= github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
github.com/tidwall/gjson v1.20.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc= github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM= github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
@ -63,37 +73,39 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s= github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=
github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo= github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4= github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg= github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
go.mau.fi/libsignal v0.2.2 h1:QV+XdzQkm3x3aSG7FcqfGSZuFXz83pRZPBFaPygHbOU= go.mau.fi/libsignal v0.2.1 h1:vRZG4EzTn70XY6Oh/pVKrQGuMHBkAWlGRC22/85m9L0=
go.mau.fi/libsignal v0.2.2/go.mod h1:CRlIQg2J8uYTfDFvNoO8/KcZjs5cey0vbc6oj/bssY0= go.mau.fi/libsignal v0.2.1/go.mod h1:iVvjrHyfQqWajOUaMEsIfo3IqgVMrhWcPiiEzk7NgoU=
go.mau.fi/util v0.11.0 h1:JNEpkVRxfTdJUC0DIGKiRlLKzl4G/qLX/eVzoP7QuwY= go.mau.fi/util v0.9.7 h1:AWGNbJfz1zRcQOKeOEYhKUG2fT+/26Gy6kyqcH8tnBg=
go.mau.fi/util v0.11.0/go.mod h1:04AEFVsc4zNdIkDC1gkdqbg/nZFolHY50pyXh2DSy7s= go.mau.fi/util v0.9.7/go.mod h1:5T2f3ZWZFAGgmFwg3dGw7YK6kIsb9lryDzvynoR98pE=
go.mau.fi/webp v0.3.0 h1:gVHQZtz21Ziwj+CDuklbX9mqpsnDIFKxs/BJyV7iZzA= go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
go.mau.fi/webp v0.3.0/go.mod h1:rlZFTev+dYxhvk+XNBP/5GcTt4gXmzAB4DU0aGUYIQo= go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
go.mau.fi/whatsmeow v0.0.0-20261026125351-1fbae988179a h1:ZyCV/mWfRkutxs5R0K3v+oyTbF4huprSlPZg/LEFQ7k= go.mau.fi/whatsmeow v0.0.0-20260305215846-fc65416c22c4 h1:FGA3NtCVNeCJ+C+KBg1pODsrfxC/trM3RHFWIeY7y4c=
go.mau.fi/whatsmeow v0.0.0-20261026125351-1fbae988179a/go.mod h1:iY4lm61nTLWsNiIE1EmHUZwhKGBeoV4dhnVE2uMtNoQ= go.mau.fi/whatsmeow v0.0.0-20260305215846-fc65416c22c4/go.mod h1:mXCRFyPEPn4jqWz6Afirn8vY7DpHCPnlKq6I2cWwFHM=
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU= go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w= go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e h1:HgI6W1OolKK6tcV74is+rl9SMT0njsHlp0TbQiGXI/A= golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e/go.mod h1:DBoxwWhjaW8DJJw/YeM4zpeOKMC1FMXSblsYy+1mFSw= golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
golang.org/x/image v0.47.0 h1:za/8gVz++PPZqmZJBNSISICN5Rrj5zixP/8K++fgmqo= golang.org/x/image v0.37.0 h1:ZiRjArKI8GwxZOoEtUfhrBtaCN+4b/7709dlT6SSnQA=
golang.org/x/image v0.47.0/go.mod h1:IkN5o4A7vq55vXN8FWjjnhg9Rs2GOtffqsSi9mWAKYE= golang.org/x/image v0.37.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg= golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs= golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA= golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
@ -103,5 +115,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M= maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA= maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
maunium.net/go/mautrix v0.32.1-0.20261105141340-842796acf445 h1:ZI9Y2YEmjqQDloOCiciHeynTRYjUwK0ZZbNcRH/5NOc= maunium.net/go/mautrix v0.26.5-0.20260322102453-0c955c396df7 h1:KUhlBHWGgknqYC2V8di4DFNh73atDtgPlqqO5FoLmPc=
maunium.net/go/mautrix v0.32.1-0.20261105141340-842796acf445/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU= maunium.net/go/mautrix v0.26.5-0.20260322102453-0c955c396df7/go.mod h1:YWw8NWTszsbyFAznboicBObwHPgTSLcuTbVX2kY7U2M=

View file

@ -4,7 +4,6 @@ import (
"context" "context"
"errors" "errors"
"fmt" "fmt"
"runtime/debug"
"slices" "slices"
"strconv" "strconv"
"strings" "strings"
@ -13,8 +12,6 @@ import (
"time" "time"
"github.com/rs/zerolog" "github.com/rs/zerolog"
"go.mau.fi/util/exerrors"
"go.mau.fi/util/exmaps"
"go.mau.fi/util/ptr" "go.mau.fi/util/ptr"
"go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/proto/waE2E" "go.mau.fi/whatsmeow/proto/waE2E"
@ -64,7 +61,7 @@ func (wa *WhatsAppClient) historySyncLoop(ctx context.Context) {
} }
}() }()
for { for {
var resetTimer, errored bool var resetTimer bool
select { select {
case <-wa.historySyncWakeup: case <-wa.historySyncWakeup:
dispatchTimer.Stop() dispatchTimer.Stop()
@ -74,21 +71,12 @@ func (wa *WhatsAppClient) historySyncLoop(ctx context.Context) {
} else if notif == nil { } else if notif == nil {
wa.UserLogin.Log.Debug().Msg("No more queued history sync notifications") wa.UserLogin.Log.Debug().Msg("No more queued history sync notifications")
} else { } else {
resetTimer, errored = wa.downloadAndSaveWAHistorySyncData(ctx, notif, rowid) resetTimer = wa.downloadAndSaveWAHistorySyncData(ctx, notif, rowid)
// Continue waking up the loop until all queued notifications are processed // Continue waking up the loop until all queued notifications are processed
select { select {
case wa.historySyncWakeup <- struct{}{}: case wa.historySyncWakeup <- struct{}{}:
default: default:
} }
if errored {
wa.UserLogin.Log.Debug().Msg("Temporarily stopping history sync loop after error")
select {
case <-time.After(1 * time.Minute):
case <-ctx.Done():
wa.UserLogin.Log.Debug().Msg("Stopping main history sync loop")
return
}
}
} }
case <-ctx.Done(): case <-ctx.Done():
wa.UserLogin.Log.Debug().Msg("Stopping main history sync loop") wa.UserLogin.Log.Debug().Msg("Stopping main history sync loop")
@ -120,54 +108,21 @@ func (wa *WhatsAppClient) saveWAHistorySyncNotification(ctx context.Context, evt
} }
} }
func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context, evt *waE2E.HistorySyncNotification, rowid int) (resetTimer, errored bool) { func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context, evt *waE2E.HistorySyncNotification, rowid int) (resetTimer bool) {
log := wa.UserLogin.Log.With(). log := wa.UserLogin.Log.With().
Str("action", "download history sync"). Str("action", "download history sync").
Stringer("sync_type", evt.GetSyncType()). Stringer("sync_type", evt.GetSyncType()).
Uint32("chunk_order", evt.GetChunkOrder()). Uint32("chunk_order", evt.GetChunkOrder()).
Uint32("progress", evt.GetProgress()). Uint32("progress", evt.GetProgress()).
Logger() Logger()
defer func() { log.Debug().Msg("Downloading history sync")
if v := recover(); v != nil {
log.Err(exerrors.RecoverToError(v)).
Bytes(zerolog.ErrorStackFieldName, debug.Stack()).
Msg("History sync saving panicked")
errored = true
}
}()
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")
blob, err := wa.Client.DownloadHistorySync(log.WithContext(ctx), evt, true) blob, err := wa.Client.DownloadHistorySync(log.WithContext(ctx), evt, true)
if errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith403) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith410) { if err != nil {
log.Err(err).Msg("History sync blob is no longer available, dropping notification")
if err = wa.Main.DB.HSNotif.Delete(ctx, rowid); err != nil {
log.Err(err).Msg("Failed to delete queued history sync notification")
errored = true
}
return
} else if err != nil {
log.Err(err).Msg("Failed to download history sync") log.Err(err).Msg("Failed to download history sync")
errored = true
return
}
if blob.GetSyncType() == waHistorySync.HistorySync_ON_DEMAND {
wa.finishOnDemandHistorySyncRequest(evt.GetPeerDataRequestSessionID())
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 return
} }
err = wa.Main.DB.DoTxn(ctx, nil, func(ctx context.Context) (innerErr error) { err = wa.Main.DB.DoTxn(ctx, nil, func(ctx context.Context) (innerErr error) {
innerErr = wa.handleWAHistorySync(ctx, evt, blob, true) resetTimer, innerErr = wa.handleWAHistorySync(ctx, evt, blob, true)
if innerErr != nil { if innerErr != nil {
return return
} }
@ -179,17 +134,6 @@ func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context,
}) })
if err != nil { if err != nil {
log.Err(err).Msg("Failed to store history sync notification data") log.Err(err).Msg("Failed to store history sync notification data")
errored = true
} else {
resetTimer = blob.GetSyncType() == waHistorySync.HistorySync_INITIAL_BOOTSTRAP ||
blob.GetSyncType() == waHistorySync.HistorySync_RECENT ||
blob.GetSyncType() == waHistorySync.HistorySync_FULL
err = wa.Client.DeleteMedia(ctx, whatsmeow.MediaHistory, evt.GetDirectPath(), evt.GetFileEncSHA256(), evt.GetEncHandle())
if err != nil {
log.Err(err).Msg("Failed to delete history sync blob from server")
} else {
log.Debug().Msg("Deleted history sync blob from server")
}
} }
return return
} }
@ -199,9 +143,9 @@ func (wa *WhatsAppClient) handleWAHistorySync(
notif *waE2E.HistorySyncNotification, notif *waE2E.HistorySyncNotification,
evt *waHistorySync.HistorySync, evt *waHistorySync.HistorySync,
stopOnError bool, stopOnError bool,
) error { ) (bool, error) {
if evt == nil || evt.SyncType == nil { if evt == nil || evt.SyncType == nil {
return nil return false, nil
} }
log := wa.UserLogin.Log.With(). log := wa.UserLogin.Log.With().
Str("action", "store history sync"). Str("action", "store history sync").
@ -226,7 +170,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
Int("recent_sticker_count", len(evt.GetRecentStickers())). Int("recent_sticker_count", len(evt.GetRecentStickers())).
Int("past_participant_count", len(evt.GetPastParticipants())). Int("past_participant_count", len(evt.GetPastParticipants())).
Msg("Ignoring history sync") Msg("Ignoring history sync")
return nil return false, nil
} }
log.Info(). log.Info().
Int("conversation_count", len(evt.GetConversations())). Int("conversation_count", len(evt.GetConversations())).
@ -234,8 +178,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
Dict("notification_metadata", zerolog.Dict(). Dict("notification_metadata", zerolog.Dict().
Int64("oldest_msg_in_chunk_ts", notif.GetOldestMsgInChunkTimestampSec()). Int64("oldest_msg_in_chunk_ts", notif.GetOldestMsgInChunkTimestampSec()).
Any("full_request_meta", notif.GetFullHistorySyncOnDemandRequestMetadata()). Any("full_request_meta", notif.GetFullHistorySyncOnDemandRequestMetadata()).
Any("access_status", notif.GetMessageAccessStatus()). Any("access_status", notif.GetMessageAccessStatus())).
Str("peer_data_request_session_id", notif.GetPeerDataRequestSessionID())).
Msg("Storing history sync") Msg("Storing history sync")
start := time.Now() start := time.Now()
successfullySavedTotal := 0 successfullySavedTotal := 0
@ -255,42 +198,33 @@ func (wa *WhatsAppClient) handleWAHistorySync(
} else if jid.Server == types.BroadcastServer { } else if jid.Server == types.BroadcastServer {
log.Debug().Stringer("chat_jid", jid).Msg("Skipping broadcast list in history sync") log.Debug().Stringer("chat_jid", jid).Msg("Skipping broadcast list in history sync")
continue continue
} else if jid == types.PSAJID {
// New messages in the official updates chat aren't bridged because they use a different path,
// so drop history as well. This should be removed if the PSA chat gets properly bridged in the future.
log.Debug().Stringer("chat_jid", jid).Msg("Skipping PSA in history sync")
continue
} else { } else {
totalMessageCount += len(conv.GetMessages()) totalMessageCount += len(conv.GetMessages())
} }
if jid == types.MetaAIJID { if jid.Server == types.HiddenUserServer {
jid = types.NewMetaAIJID pn, err := wa.GetStore().LIDs.GetPNForLID(ctx, jid)
} else if jid.Server == types.DefaultUserServer {
lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, jid)
if err != nil { if err != nil {
log.Err(err).Stringer("pn", jid).Msg("Failed to get LID for phone number in history sync") log.Err(err).Stringer("lid", jid).Msg("Failed to get PN for LID in history sync")
} else if lid.IsEmpty() { } else if pn.IsEmpty() {
log.Warn().Stringer("pn", jid).Msg("No LID found for phone number in history sync") log.Warn().Stringer("lid", jid).Msg("No PN found for LID in history sync")
} else { } else {
log.Debug(). log.Debug().
Stringer("lid", lid). Stringer("lid", jid).
Stringer("pn", jid). Stringer("pn", pn).
Msg("Rerouting phone number DM to LID in history sync") Msg("Rerouting LID DM to phone number in history sync")
jid = lid jid = pn
} }
} }
log.UpdateContext(func(c zerolog.Context) zerolog.Context { log.UpdateContext(func(c zerolog.Context) zerolog.Context {
return c.Stringer("chat_jid", jid) return c.Stringer("chat_jid", jid)
}) })
var firstItemTime, lastItemTime time.Time var minTime, maxTime time.Time
var minTimeIndex, maxTimeIndex int
ignoredTypes := 0 ignoredTypes := 0
rawMessages := conv.GetMessages() messages := make([]*wadb.HistorySyncMessageTuple, 0, len(conv.GetMessages()))
messages := make([]*wadb.HistorySyncMessageTuple, 0, len(rawMessages)) for i, rawMsg := range conv.GetMessages() {
allowClamp := conv.GetCommentsCount() == 0
var newerTS uint64
for i, rawMsg := range rawMessages {
// Don't store messages that will just be skipped. // Don't store messages that will just be skipped.
msgEvt, err := wa.Client.ParseWebMessage(jid, rawMsg.GetMessage()) msgEvt, err := wa.Client.ParseWebMessage(jid, rawMsg.GetMessage())
if err != nil { if err != nil {
@ -298,43 +232,23 @@ func (wa *WhatsAppClient) handleWAHistorySync(
Int("msg_index", i). Int("msg_index", i).
Str("msg_id", rawMsg.GetMessage().GetKey().GetID()). Str("msg_id", rawMsg.GetMessage().GetKey().GetID()).
Uint64("msg_time_seconds", rawMsg.GetMessage().GetMessageTimestamp()). Uint64("msg_time_seconds", rawMsg.GetMessage().GetMessageTimestamp()).
Stringer("stub_type", rawMsg.GetMessage().GetMessageStubType()).
Str("message_type", getMessageType(rawMsg.GetMessage().GetMessage())).
Bool("from_me", rawMsg.GetMessage().GetKey().GetFromMe()).
Msg("Dropping historical message due to parse error") Msg("Dropping historical message due to parse error")
log.Trace().
Any("web_message_info", rawMsg.GetMessage()).
Msg("Content of event that failed to parse")
continue continue
} }
if firstItemTime.IsZero() { if minTime.IsZero() || msgEvt.Info.Timestamp.Before(minTime) {
firstItemTime = msgEvt.Info.Timestamp minTime = msgEvt.Info.Timestamp
minTimeIndex = i
}
if maxTime.IsZero() || msgEvt.Info.Timestamp.After(maxTime) {
maxTime = msgEvt.Info.Timestamp
maxTimeIndex = i
} }
lastItemTime = msgEvt.Info.Timestamp
msgType := getMessageType(msgEvt.Message) msgType := getMessageType(msgEvt.Message)
if msgType == "ignore" || strings.HasPrefix(msgType, "unknown_protocol_") { if msgType == "ignore" || strings.HasPrefix(msgType, "unknown_protocol_") {
ignoredTypes++ ignoredTypes++
continue continue
} }
// Comments (replies) in announcement groups are not ordered by timestamp, so don't clamp them.
if rawMsg.GetMessage().GetCommentMetadata().GetCommentParentKey() != nil {
allowClamp = false
}
// WhatsApp has bugs where some random messages will have timestamps decades in the future.
// To ensure they don't mess up our ordering, require timestamps of older messages to be
// before the previous (newer) message.
if currentTS := rawMsg.GetMessage().GetMessageTimestamp(); newerTS > 0 && allowClamp && currentTS > newerTS {
log.Warn().
Time("current_ts", time.Unix(int64(currentTS), 0)).
Time("prev_ts", time.Unix(int64(newerTS), 0)).
Int("msg_index", i).
Str("msg_id", rawMsg.GetMessage().GetKey().GetID()).
Msg("Clamping message timestamp")
rawMsg.Message.MessageTimestamp = ptr.Ptr(newerTS)
msgEvt.Info.Timestamp = time.Unix(int64(newerTS), 0)
}
newerTS = rawMsg.GetMessage().GetMessageTimestamp()
marshaled, err := proto.Marshal(rawMsg) marshaled, err := proto.Marshal(rawMsg)
if err != nil { if err != nil {
log.Warn().Err(err). log.Warn().Err(err).
@ -349,8 +263,10 @@ func (wa *WhatsAppClient) handleWAHistorySync(
log.Debug(). log.Debug().
Int("wrapped_count", len(messages)). Int("wrapped_count", len(messages)).
Int("ignored_msg_type_count", ignoredTypes). Int("ignored_msg_type_count", ignoredTypes).
Time("first_item_time", firstItemTime). Time("lowest_time", minTime).
Time("last_item_time", lastItemTime). Int("lowest_time_index", minTimeIndex).
Time("highest_time", maxTime).
Int("highest_time_index", maxTimeIndex).
Dict("metadata", zerolog.Dict(). Dict("metadata", zerolog.Dict().
Uint32("ephemeral_expiration", conv.GetEphemeralExpiration()). Uint32("ephemeral_expiration", conv.GetEphemeralExpiration()).
Int64("ephemeral_setting_timestamp", conv.GetEphemeralSettingTimestamp()). Int64("ephemeral_setting_timestamp", conv.GetEphemeralSettingTimestamp()).
@ -359,17 +275,15 @@ func (wa *WhatsAppClient) handleWAHistorySync(
Bool("archived", conv.GetArchived()). Bool("archived", conv.GetArchived()).
Uint32("pinned", conv.GetPinned()). Uint32("pinned", conv.GetPinned()).
Uint64("mute_end", conv.GetMuteEndTime()). Uint64("mute_end", conv.GetMuteEndTime()).
Uint32("unread_count", conv.GetUnreadCount()). Uint32("unread_count", conv.GetUnreadCount()),
Bool("end_of_history", conv.GetEndOfHistoryTransfer()).
Stringer("end_of_history_type", conv.GetEndOfHistoryTransferType()),
). ).
Msg("Collected messages to save from history sync conversation") Msg("Collected messages to save from history sync conversation")
if len(messages) > 0 { if len(messages) > 0 {
err = wa.Main.DB.Conversation.Put(ctx, wadb.NewConversation(wa.UserLogin.ID, jid, conv, firstItemTime)) err = wa.Main.DB.Conversation.Put(ctx, wadb.NewConversation(wa.UserLogin.ID, jid, conv, maxTime))
if err != nil { if err != nil {
if stopOnError { if stopOnError {
return fmt.Errorf("failed to save conversation metadata for %s: %w", jid, err) return false, fmt.Errorf("failed to save conversation metadata for %s: %w", jid, err)
} }
log.Err(err).Msg("Failed to save conversation metadata") log.Err(err).Msg("Failed to save conversation metadata")
continue continue
@ -377,7 +291,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
err = wa.Main.DB.Message.Put(ctx, wa.UserLogin.ID, jid, messages) err = wa.Main.DB.Message.Put(ctx, wa.UserLogin.ID, jid, messages)
if err != nil { if err != nil {
if stopOnError { if stopOnError {
return fmt.Errorf("failed to save messages in %s: %w", jid, err) return false, fmt.Errorf("failed to save messages in %s: %w", jid, err)
} }
log.Err(err).Msg("Failed to save messages") log.Err(err).Msg("Failed to save messages")
failedToSaveTotal += len(messages) failedToSaveTotal += len(messages)
@ -387,7 +301,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
err = wa.Main.Bridge.DB.BackfillTask.MarkNotDone(ctx, wa.makeWAPortalKey(jid), wa.UserLogin.ID) err = wa.Main.Bridge.DB.BackfillTask.MarkNotDone(ctx, wa.makeWAPortalKey(jid), wa.UserLogin.ID)
if err != nil { if err != nil {
if stopOnError { if stopOnError {
return fmt.Errorf("failed to mark backfill task as not done for %s: %w", jid, err) return false, fmt.Errorf("failed to mark backfill task as not done for %s: %w", jid, err)
} }
log.Err(err).Msg("Failed to mark backfill task as not done") log.Err(err).Msg("Failed to mark backfill task as not done")
} }
@ -399,20 +313,15 @@ func (wa *WhatsAppClient) handleWAHistorySync(
Int("total_message_count", totalMessageCount). Int("total_message_count", totalMessageCount).
Dur("duration", time.Since(start)). Dur("duration", time.Since(start)).
Msg("Finished storing history sync") Msg("Finished storing history sync")
return nil resetTimer := evt.GetSyncType() == waHistorySync.HistorySync_RECENT ||
evt.GetSyncType() == waHistorySync.HistorySync_FULL
return resetTimer, nil
} }
func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) { func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) {
log := wa.UserLogin.Log.With(). log := wa.UserLogin.Log.With().
Str("action", "create portals from history sync"). Str("action", "create portals from history sync").
Logger() Logger()
defer func() {
if v := recover(); v != nil {
log.Err(exerrors.RecoverToError(v)).
Bytes(zerolog.ErrorStackFieldName, debug.Stack()).
Msg("History sync portal creation panicked")
}
}()
ctx = log.WithContext(ctx) ctx = log.WithContext(ctx)
limit := wa.Main.Config.HistorySync.MaxInitialConversations limit := wa.Main.Config.HistorySync.MaxInitialConversations
loginTS := wa.UserLogin.Metadata.(*waid.UserLoginMetadata).LoggedInAt loginTS := wa.UserLogin.Metadata.(*waid.UserLoginMetadata).LoggedInAt
@ -454,8 +363,8 @@ func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) {
return return
} }
wrappedInfo, err := wa.getChatInfo(ctx, conv.ChatJID, conv, true) wrappedInfo, err := wa.getChatInfo(ctx, conv.ChatJID, conv, true)
if errors.Is(err, whatsmeow.ErrNotInGroup) || errors.Is(err, whatsmeow.ErrGroupNotFound) || errors.Is(err, ErrNewsletterNotFound) { if errors.Is(err, whatsmeow.ErrNotInGroup) {
log.Debug().Err(err).Stringer("chat_jid", conv.ChatJID). log.Debug().Stringer("chat_jid", conv.ChatJID).
Msg("Skipping creating room because the user is not a participant") Msg("Skipping creating room because the user is not a participant")
//err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, conv.ChatJID) //err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, conv.ChatJID)
//if err != nil { //if err != nil {
@ -528,39 +437,26 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
if err != nil { if err != nil {
return nil, err return nil, err
} }
if portalJID.Server == types.DefaultUserServer {
zerolog.Ctx(ctx).Warn().Stringer("portal_jid", portalJID).Msg("FetchMessages called for phone number portal")
}
var markRead bool var markRead bool
var startTime, endTime *time.Time var startTime, endTime *time.Time
var conv *wadb.Conversation
if params.Forward || wa.Main.Config.HistorySync.BackwardsOnDemand {
conv, err = wa.Main.DB.Conversation.Get(ctx, wa.UserLogin.ID, portalJID)
if err != nil {
return nil, fmt.Errorf("failed to get conversation from database: %w", err)
}
}
if params.Forward { if params.Forward {
if params.AnchorMessage != nil { if params.AnchorMessage != nil {
startTime = ptr.Ptr(params.AnchorMessage.Timestamp) startTime = ptr.Ptr(params.AnchorMessage.Timestamp)
} }
if conv != nil { conv, err := wa.Main.DB.Conversation.Get(ctx, wa.UserLogin.ID, portalJID)
if err != nil {
return nil, fmt.Errorf("failed to get conversation from database: %w", err)
} else if conv != nil {
markRead = !ptr.Val(conv.MarkedAsUnread) && ptr.Val(conv.UnreadCount) == 0 markRead = !ptr.Val(conv.MarkedAsUnread) && ptr.Val(conv.UnreadCount) == 0
} }
} else { } else if params.Cursor != "" {
if params.AnchorMessage != nil {
endTime = ptr.Ptr(params.AnchorMessage.Timestamp)
}
if params.Cursor != "" {
endTimeUnix, err := strconv.ParseInt(string(params.Cursor), 10, 64) endTimeUnix, err := strconv.ParseInt(string(params.Cursor), 10, 64)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to parse cursor: %w", err) return nil, fmt.Errorf("failed to parse cursor: %w", err)
} }
cursorTime := time.Unix(endTimeUnix, 0) endTime = ptr.Ptr(time.Unix(endTimeUnix, 0))
if endTime == nil || cursorTime.Before(*endTime) { } else if params.AnchorMessage != nil {
endTime = &cursorTime endTime = ptr.Ptr(params.AnchorMessage.Timestamp)
}
}
} }
var anchorID types.MessageID var anchorID types.MessageID
if params.AnchorMessage != nil { if params.AnchorMessage != nil {
@ -569,32 +465,19 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
anchorID = parsedID.ID anchorID = parsedID.ID
} }
} }
var hasMore bool
if !params.Forward && wa.Main.Config.HistorySync.BackwardsOnDemand {
hasMore = conv != nil && ptr.Val(conv.EndOfHistoryTransferType) == waHistorySync.Conversation_COMPLETE_BUT_MORE_MESSAGES_REMAIN_ON_PRIMARY
}
messages, err := wa.Main.DB.Message.GetBetween(ctx, wa.UserLogin.ID, portalJID, startTime, endTime, params.Count+1) messages, err := wa.Main.DB.Message.GetBetween(ctx, wa.UserLogin.ID, portalJID, startTime, endTime, params.Count+1)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to load messages from database: %w", err) return nil, fmt.Errorf("failed to load messages from database: %w", err)
} else if len(messages) == 0 || (len(messages) == 1 && anchorID != "" && messages[0].GetKey().GetID() == anchorID) { } else if len(messages) == 0 || (len(messages) == 1 && anchorID != "" && messages[0].GetKey().GetID() == anchorID) {
wa.deleteHistorySyncMessages(ctx, portalJID, 0, 0)
hasMore = hasMore && params.AnchorMessage != nil
if hasMore && !params.AllowSlowFetch {
return &bridgev2.FetchMessagesResponse{
MoreRequiresSlowFetch: true,
HasMore: true,
Forward: params.Forward,
}, nil
} else if hasMore {
return wa.fetchMessagesFromPhone(ctx, portalJID, params)
}
return &bridgev2.FetchMessagesResponse{ return &bridgev2.FetchMessagesResponse{
HasMore: false, HasMore: false,
Forward: params.Forward, Forward: params.Forward,
}, nil }, nil
} }
if len(messages) > params.Count { hasMore := false
oldestTS := messages[len(messages)-1].GetMessageTimestamp() oldestTS := messages[len(messages)-1].GetMessageTimestamp()
newestTS := messages[0].GetMessageTimestamp()
if len(messages) > params.Count {
hasMore = true hasMore = true
// For safety, cut off messages with the oldest timestamp in the response. // For safety, cut off messages with the oldest timestamp in the response.
// Otherwise, if there are multiple messages with the same timestamp, the next fetch may miss some. // Otherwise, if there are multiple messages with the same timestamp, the next fetch may miss some.
@ -605,118 +488,19 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
} }
} }
} }
resp, err := wa.convertHistorySyncMessages(ctx, params.Portal, portalJID, messages, true, !params.Forward && !hasMore) convertedMessages := make([]*bridgev2.BackfillMessage, len(messages))
if err != nil {
return nil, fmt.Errorf("failed to convert messages: %w", err)
}
// TODO might be better to always set true here and let GetBetween return an empty result to mark as false?
resp.HasMore = hasMore
resp.Forward = params.Forward
resp.MarkRead = markRead
if params.Forward && !markRead && conv != nil && !ptr.Val(conv.MarkedAsUnread) {
resp.ReadUpTo = historySyncReadUpTo(resp.Messages, int(ptr.Val(conv.UnreadCount)))
}
return resp, nil
}
func historySyncReadUpTo(messages []*bridgev2.BackfillMessage, unreadCount int) time.Time {
if unreadCount <= 0 {
return time.Time{}
}
for i := len(messages) - 1; i >= 0; i-- {
if messages[i].Sender.IsFromMe {
continue
}
unreadCount--
if unreadCount == 0 {
return messages[i].Timestamp.Add(-time.Nanosecond)
}
}
return time.Time{}
}
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,
deleteEverything bool,
) (*bridgev2.FetchMessagesResponse, error) {
cli := wa.Client
if cli == nil {
return nil, bridgev2.ErrNotLoggedIn
}
oldestTS := messages[len(messages)-1].GetMessageTimestamp()
newestTS := messages[0].GetMessageTimestamp()
convertedMessages := make([]*bridgev2.BackfillMessage, 0, len(messages))
var mediaRequests []*wadb.MediaRequest var mediaRequests []*wadb.MediaRequest
dups := make(exmaps.Set[networkid.MessageID])
for i, msg := range messages { for i, msg := range messages {
evt, err := cli.ParseWebMessage(portalJID, msg) evt, err := wa.Client.ParseWebMessage(portalJID, msg)
if err != nil { if err != nil {
if explodeOnError {
// This should never happen because the info is already parsed once before being stored in the database // This should never happen because the info is already parsed once before being stored in the database
return nil, fmt.Errorf("failed to parse info of message %s: %w", msg.GetKey().GetID(), err) return nil, fmt.Errorf("failed to parse info of message %s: %w", msg.GetKey().GetID(), err)
} }
zerolog.Ctx(ctx).Warn().Err(err). var mediaReq *wadb.MediaRequest
Int("msg_index", i).
Str("msg_id", msg.GetKey().GetID()).
Uint64("msg_time_seconds", msg.GetMessageTimestamp()).
Msg("Dropping historical message due to parse error")
zerolog.Ctx(ctx).Trace().
Any("web_message_info", msg.GetMessage()).
Msg("Content of event that failed to parse")
continue
}
if !explodeOnError {
msgType := getMessageType(evt.Message)
if msgType == "ignore" || strings.HasPrefix(msgType, "unknown_protocol_") {
continue
}
}
if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, false) {
return nil, fmt.Errorf("failed to ensure alt JIDs for message %s", evt.Info.ID)
}
if evt.Info.IsFromMe {
wa.addPreviousOwnLID(ctx, evt.Info.Sender)
}
isViewOnce := evt.IsViewOnce || evt.IsViewOnceV2 || evt.IsViewOnceV2Extension isViewOnce := evt.IsViewOnce || evt.IsViewOnceV2 || evt.IsViewOnceV2Extension
converted, mediaReq := wa.convertHistorySyncMessage( convertedMessages[i], mediaReq = wa.convertHistorySyncMessage(
ctx, cli, portal, &evt.Info, evt.Message, evt.RawMessage, isViewOnce, msg.Reactions, ctx, params.Portal, &evt.Info, evt.Message, evt.RawMessage, isViewOnce, msg.Reactions,
) )
// This is a hack to remove duplicates where the same message is inserted with both the LID and phone number sender
// TODO prevent those being inserted in the first place instead of hacking around it here
if !dups.Add(converted.ID) {
continue
}
convertedMessages = append(convertedMessages, converted)
if mediaReq != nil { if mediaReq != nil {
mediaRequests = append(mediaRequests, mediaReq) mediaRequests = append(mediaRequests, mediaReq)
} }
@ -725,11 +509,23 @@ func (wa *WhatsAppClient) convertHistorySyncMessages(
return &bridgev2.FetchMessagesResponse{ return &bridgev2.FetchMessagesResponse{
Messages: convertedMessages, Messages: convertedMessages,
Cursor: networkid.PaginationCursor(strconv.FormatUint(oldestTS, 10)), Cursor: networkid.PaginationCursor(strconv.FormatUint(oldestTS, 10)),
HasMore: hasMore,
Forward: endTime == nil,
MarkRead: markRead,
// TODO set remaining or total count
CompleteCallback: func() { CompleteCallback: func() {
if deleteEverything { // TODO this only deletes after backfilling. If there's no need for backfill after a relogin,
wa.deleteHistorySyncMessages(ctx, portalJID, 1<<62, oldestTS) // the messages will be stuck in the database
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 { } else {
wa.deleteHistorySyncMessages(ctx, portalJID, newestTS, oldestTS) // Otherwise just delete the messages that got backfilled
err = wa.Main.DB.Message.DeleteBetween(ctx, wa.UserLogin.ID, portalJID, newestTS, oldestTS)
}
if err != nil {
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to delete messages from database after backfill")
} }
if len(mediaRequests) > 0 { if len(mediaRequests) > 0 {
go func(ctx context.Context) { go func(ctx context.Context) {
@ -748,164 +544,8 @@ func (wa *WhatsAppClient) convertHistorySyncMessages(
}, nil }, nil
} }
func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, portalJID types.JID, params bridgev2.FetchMessagesParams) (*bridgev2.FetchMessagesResponse, error) {
if params.AnchorMessage == nil {
return nil, fmt.Errorf("anchor message is required to fetch messages from phone")
}
anchor := params.AnchorMessage
parsed, err := waid.ParseMessageID(anchor.ID)
if err != nil {
anchor, err = wa.Main.Bridge.DB.Message.GetFirstPortalMessageExcludingIDPattern(ctx, params.Portal.PortalKey, "fake:%")
if err != nil {
return nil, fmt.Errorf("failed to get first non-fake portal message: %w", err)
} else if anchor == nil {
return &bridgev2.FetchMessagesResponse{}, nil
}
parsed, err = waid.ParseMessageID(anchor.ID)
if err != nil {
return nil, fmt.Errorf("failed to parse anchor message ID: %w", err)
}
}
msgID := wa.Client.GenerateMessageID()
if !wa.startOnDemandHistorySyncRequest(ctx, portalJID, msgID) {
return &bridgev2.FetchMessagesResponse{
HasMore: true,
Pending: true,
}, nil
}
reqData := wa.Client.BuildHistorySyncRequest(&types.MessageInfo{
MessageSource: types.MessageSource{
Chat: portalJID,
Sender: parsed.Sender,
IsFromMe: wa.IsOwnJID(parsed.Sender),
IsGroup: parsed.Chat.Server == types.GroupServer,
},
ID: parsed.ID,
Timestamp: anchor.Timestamp,
}, 50)
zerolog.Ctx(ctx).Debug().
Str("request_msg_id", msgID).
Stringer("portal_jid", portalJID).
Any("anchor_msg_parsed", parsed).
Any("request_data", reqData).
Msg("Sending history sync request")
_, err = wa.Client.SendMessage(ctx, wa.JID.ToNonAD(), reqData, whatsmeow.SendRequestExtra{
ID: msgID,
Peer: true,
})
if err != nil {
wa.onDemandHistorySyncLock.Lock()
delete(wa.onDemandHistorySyncRequests, portalJID)
wa.onDemandHistorySyncLock.Unlock()
return nil, fmt.Errorf("failed to send history sync request: %w", err)
}
return &bridgev2.FetchMessagesResponse{
HasMore: true,
Pending: true,
}, nil
}
type onDemandHistorySyncRequest struct {
ID types.MessageID
SentAt time.Time
}
const onDemandHistorySyncRequestTimeout = 1 * time.Hour
func (wa *WhatsAppClient) startOnDemandHistorySyncRequest(ctx context.Context, portalJID types.JID, msgID types.MessageID) bool {
wa.onDemandHistorySyncLock.Lock()
defer wa.onDemandHistorySyncLock.Unlock()
if req, inFlight := wa.onDemandHistorySyncRequests[portalJID]; inFlight && time.Since(req.SentAt) < onDemandHistorySyncRequestTimeout {
zerolog.Ctx(ctx).Debug().
Str("in_flight_request_msg_id", req.ID).
Stringer("portal_jid", portalJID).
Time("sent_at", req.SentAt).
Msg("Not sending history sync request, previous request is still in flight")
return false
}
wa.onDemandHistorySyncRequests[portalJID] = onDemandHistorySyncRequest{ID: msgID, SentAt: time.Now()}
return true
}
func (wa *WhatsAppClient) finishOnDemandHistorySyncRequest(sessionID string) {
wa.onDemandHistorySyncLock.Lock()
defer wa.onDemandHistorySyncLock.Unlock()
for chatJID, req := range wa.onDemandHistorySyncRequests {
if req.ID == sessionID {
delete(wa.onDemandHistorySyncRequests, chatJID)
}
}
}
func (wa *WhatsAppClient) handleOnDemandHistorySync(ctx context.Context, blob *waHistorySync.HistorySync) {
if len(blob.GetConversations()) > 1 {
zerolog.Ctx(ctx).Warn().
Int("conversation_count", len(blob.GetConversations())).
Msg("Received on-demand history sync with multiple conversations")
}
for _, conv := range blob.GetConversations() {
portalJID, err := types.ParseJID(conv.GetID())
if err != nil {
zerolog.Ctx(ctx).Err(err).Str("jid", conv.GetID()).Msg("Failed to parse portal JID")
continue
}
if portalJID == types.MetaAIJID {
portalJID = types.NewMetaAIJID
} else if portalJID.Server == types.DefaultUserServer {
lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, portalJID)
if err != nil {
zerolog.Ctx(ctx).Err(err).Stringer("lid", portalJID).Msg("Failed to get LID for phone number in on-demand history sync")
} else if lid.IsEmpty() {
zerolog.Ctx(ctx).Warn().Stringer("lid", portalJID).Msg("No LID found for phone number in on-demand history sync")
} else {
zerolog.Ctx(ctx).Debug().
Stringer("lid", lid).
Stringer("pn", portalJID).
Msg("Rerouting phone number DM to LID in on-demand history sync")
portalJID = lid
}
}
portal, err := wa.Main.Bridge.GetPortalByKey(ctx, wa.makeWAPortalKey(portalJID))
if err != nil {
zerolog.Ctx(ctx).Err(err).Stringer("portal_jid", portalJID).Msg("Failed to get portal for on-demand history sync")
continue
}
ctx := zerolog.Ctx(ctx).With().
Str("portal_id", string(portal.ID)).
Str("portal_receiver", string(portal.Receiver)).
Stringer("portal_mxid", portal.MXID).
Logger().WithContext(ctx)
portal.HandleRemoteBackfill(ctx, wa.UserLogin, &simplevent.Backfill{
EventMeta: simplevent.EventMeta{
Type: bridgev2.RemoteEventBackfill,
PortalKey: portal.PortalKey,
},
GetDataFunc: func(ctx context.Context, portal *bridgev2.Portal) (*bridgev2.FetchMessagesResponse, error) {
if len(conv.GetMessages()) == 0 {
return &bridgev2.FetchMessagesResponse{}, nil
}
messages := make([]*waWeb.WebMessageInfo, len(conv.GetMessages()))
for i, rawMsg := range conv.GetMessages() {
messages[i] = rawMsg.Message
}
zerolog.Ctx(ctx).Debug().
Int("message_count", len(messages)).
Stringer("end_of_history_type", conv.GetEndOfHistoryTransferType()).
Msg("Converting messages to bridge from on-demand history sync")
resp, err := wa.convertHistorySyncMessages(ctx, portal, portalJID, messages, false, false)
if err != nil {
return nil, err
}
resp.HasMore = conv.GetEndOfHistoryTransferType() == waHistorySync.Conversation_COMPLETE_BUT_MORE_MESSAGES_REMAIN_ON_PRIMARY
return resp, nil
},
})
}
}
func (wa *WhatsAppClient) convertHistorySyncMessage( func (wa *WhatsAppClient) convertHistorySyncMessage(
ctx context.Context, cli *whatsmeow.Client, portal *bridgev2.Portal, info *types.MessageInfo, msg, rawMsg *waE2E.Message, isViewOnce bool, reactions []*waWeb.Reaction, ctx context.Context, portal *bridgev2.Portal, info *types.MessageInfo, msg, rawMsg *waE2E.Message, isViewOnce bool, reactions []*waWeb.Reaction,
) (*bridgev2.BackfillMessage, *wadb.MediaRequest) { ) (*bridgev2.BackfillMessage, *wadb.MediaRequest) {
// New messages turn these into edits, but in backfill we only have the last version, // New messages turn these into edits, but in backfill we only have the last version,
// so no need to do the edit thing. Instead, just unwrap the message. // so no need to do the edit thing. Instead, just unwrap the message.
@ -914,12 +554,11 @@ func (wa *WhatsAppClient) convertHistorySyncMessage(
} }
// TODO use proper intent // TODO use proper intent
intent := wa.Main.Bridge.Bot intent := wa.Main.Bridge.Bot
msgID := waid.MakeMessageIDWithAltSender(info.Chat, info.Sender, info.SenderAlt, info.ID)
wrapped := &bridgev2.BackfillMessage{ wrapped := &bridgev2.BackfillMessage{
ConvertedMessage: wa.Main.MsgConv.ToMatrix(ctx, portal, cli, intent, msg, rawMsg, info, isViewOnce, true, nil), ConvertedMessage: wa.Main.MsgConv.ToMatrix(ctx, portal, wa.Client, intent, msg, rawMsg, info, isViewOnce, true, nil),
Sender: wa.makeEventSender(ctx, pickLID(info.Sender, info.SenderAlt)), Sender: wa.makeEventSender(ctx, info.Sender),
ID: msgID, ID: waid.MakeMessageID(info.Chat, info.Sender, info.ID),
TxnID: networkid.TransactionID(msgID), TxnID: networkid.TransactionID(waid.MakeMessageID(info.Chat, info.Sender, info.ID)),
Timestamp: info.Timestamp, Timestamp: info.Timestamp,
StreamOrder: info.Timestamp.Unix(), StreamOrder: info.Timestamp.Unix(),
Reactions: make([]*bridgev2.BackfillReaction, 0, len(reactions)), Reactions: make([]*bridgev2.BackfillReaction, 0, len(reactions)),
@ -928,10 +567,10 @@ func (wa *WhatsAppClient) convertHistorySyncMessage(
for _, reaction := range reactions { for _, reaction := range reactions {
var sender types.JID var sender types.JID
if reaction.GetKey().GetFromMe() { if reaction.GetKey().GetFromMe() {
sender = wa.GetLID() sender = wa.JID
} else if reaction.GetKey().GetParticipant() != "" { } else if reaction.GetKey().GetParticipant() != "" {
sender, _ = types.ParseJID(*reaction.Key.Participant) sender, _ = types.ParseJID(*reaction.Key.Participant)
} else if info.Chat.Server == types.DefaultUserServer || info.Chat.Server == types.HiddenUserServer || info.Chat.Server == types.BotServer { } else if info.Chat.Server == types.DefaultUserServer || info.Chat.Server == types.BotServer {
sender = info.Chat sender = info.Chat
} }
if sender.IsEmpty() { if sender.IsEmpty() {

View file

@ -19,7 +19,6 @@ var WhatsAppGeneralCaps = &bridgev2.NetworkGeneralCapabilities{
AggressiveUpdateInfo: true, AggressiveUpdateInfo: true,
ImplicitReadReceipts: true, ImplicitReadReceipts: true,
Provisioning: bridgev2.ProvisioningCapabilities{ Provisioning: bridgev2.ProvisioningCapabilities{
ImagePackImport: true,
ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{ ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{
CreateDM: true, CreateDM: true,
LookupPhone: true, LookupPhone: true,
@ -52,7 +51,7 @@ func (wa *WhatsAppConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilit
} }
func (wa *WhatsAppConnector) GetBridgeInfoVersion() (info, caps int) { func (wa *WhatsAppConnector) GetBridgeInfoVersion() (info, caps int) {
return 1, 10 return 1, 7
} }
const WAMaxFileSize = 2000 * 1024 * 1024 const WAMaxFileSize = 2000 * 1024 * 1024
@ -67,7 +66,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel {
} }
func capID() string { func capID() string {
base := "fi.mau.whatsapp.capabilities.2026_10_01" base := "fi.mau.whatsapp.capabilities.2025_12_15"
if ffmpeg.Supported() { if ffmpeg.Supported() {
return base + "+ffmpeg" return base + "+ffmpeg"
} }
@ -126,10 +125,10 @@ var whatsappCaps = &event.RoomFeatures{
event.CapMsgSticker: { event.CapMsgSticker: {
MimeTypes: map[string]event.CapabilitySupportLevel{ MimeTypes: map[string]event.CapabilitySupportLevel{
"image/webp": event.CapLevelFullySupported, "image/webp": event.CapLevelFullySupported,
// TODO see if sending lottie is possible
//"video/lottie+json": event.CapLevelFullySupported,
"image/png": event.CapLevelPartialSupport, "image/png": event.CapLevelPartialSupport,
"image/jpeg": event.CapLevelPartialSupport, "image/jpeg": event.CapLevelPartialSupport,
// This will only be accepted if it was imported from WhatsApp
"video/lottie+json": event.CapLevelPartialSupport,
}, },
Caption: event.CapLevelDropped, Caption: event.CapLevelDropped,
MaxSize: WAMaxFileSize, MaxSize: WAMaxFileSize,
@ -177,11 +176,6 @@ var whatsappCaps = &event.RoomFeatures{
MaxTextLength: MaxTextLength, MaxTextLength: MaxTextLength,
LocationMessage: event.CapLevelFullySupported, LocationMessage: event.CapLevelFullySupported,
Poll: event.CapLevelFullySupported, Poll: event.CapLevelFullySupported,
PollEnd: event.CapLevelUnsupported,
PollHiddenVotes: event.CapLevelUnsupported,
PollDuplicateOptions: event.CapLevelUnsupported,
PollMaxOptions: 12,
PollOptionMaxLength: 100,
Reply: event.CapLevelFullySupported, Reply: event.CapLevelFullySupported,
Edit: event.CapLevelFullySupported, Edit: event.CapLevelFullySupported,
EditMaxAge: ptr.Ptr(jsontime.S(EditMaxAge)), EditMaxAge: ptr.Ptr(jsontime.S(EditMaxAge)),
@ -198,7 +192,6 @@ var whatsappCaps = &event.RoomFeatures{
var whatsappDMCaps *event.RoomFeatures var whatsappDMCaps *event.RoomFeatures
var whatsappCAGCaps *event.RoomFeatures var whatsappCAGCaps *event.RoomFeatures
var whatsappNoteToSelfCaps *event.RoomFeatures
func init() { func init() {
whatsappDMCaps = ptr.Clone(whatsappCaps) whatsappDMCaps = ptr.Clone(whatsappCaps)
@ -206,13 +199,7 @@ func init() {
whatsappDMCaps.State = event.StateFeatureMap{ whatsappDMCaps.State = event.StateFeatureMap{
event.StateBeeperDisappearingTimer.Type: {Level: event.CapLevelFullySupported}, event.StateBeeperDisappearingTimer.Type: {Level: event.CapLevelFullySupported},
} }
whatsappDMCaps.BlockUser = true
whatsappDMCaps.MemberActions = nil whatsappDMCaps.MemberActions = nil
whatsappNoteToSelfCaps = ptr.Clone(whatsappDMCaps)
whatsappNoteToSelfCaps.ID = capID() + "+self"
whatsappNoteToSelfCaps.BlockUser = false
whatsappCAGCaps = ptr.Clone(whatsappCaps) whatsappCAGCaps = ptr.Clone(whatsappCaps)
whatsappCAGCaps.ID = capID() + "+cag" whatsappCAGCaps.ID = capID() + "+cag"
whatsappCAGCaps.Reply = event.CapLevelUnsupported whatsappCAGCaps.Reply = event.CapLevelUnsupported
@ -223,10 +210,6 @@ func (wa *WhatsAppClient) GetCapabilities(ctx context.Context, portal *bridgev2.
if portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup { if portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup {
return whatsappCAGCaps return whatsappCAGCaps
} else if portal.RoomType == database.RoomTypeDM { } else if portal.RoomType == database.RoomTypeDM {
jid, err := waid.ParsePortalID(portal.ID)
if err == nil && wa.IsOwnJID(jid) {
return whatsappNoteToSelfCaps
}
return whatsappDMCaps return whatsappDMCaps
} }
return whatsappCaps return whatsappCaps

View file

@ -10,7 +10,6 @@ import (
"go.mau.fi/util/jsontime" "go.mau.fi/util/jsontime"
"go.mau.fi/util/ptr" "go.mau.fi/util/ptr"
"go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/appstate"
"go.mau.fi/whatsmeow/types" "go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events" "go.mau.fi/whatsmeow/types/events"
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
@ -30,21 +29,15 @@ func (wa *WhatsAppClient) GetChatInfo(ctx context.Context, portal *bridgev2.Port
return wa.getChatInfo(ctx, portalJID, nil, portal.MXID == "") return wa.getChatInfo(ctx, portalJID, nil, portal.MXID == "")
} }
var ErrBroadcastList = errors.New("broadcast list bridging is currently not supported")
var ErrNewsletterNotFound = errors.New("newsletter doesn't exist")
func (wa *WhatsAppClient) getChatInfo(ctx context.Context, portalJID types.JID, conv *wadb.Conversation, isNew bool) (wrapped *bridgev2.ChatInfo, err error) { func (wa *WhatsAppClient) getChatInfo(ctx context.Context, portalJID types.JID, conv *wadb.Conversation, isNew bool) (wrapped *bridgev2.ChatInfo, err error) {
switch portalJID.Server { switch portalJID.Server {
case types.DefaultUserServer, types.HiddenUserServer, types.BotServer: case types.DefaultUserServer, types.HiddenUserServer, types.BotServer:
wrapped = wa.wrapDMInfo(ctx, portalJID) wrapped = wa.wrapDMInfo(ctx, portalJID)
if portalJID == types.MuseJID {
go wa.requestMuseProfile(ctx)
}
case types.BroadcastServer: case types.BroadcastServer:
if portalJID == types.StatusBroadcastJID { if portalJID == types.StatusBroadcastJID {
wrapped = wa.wrapStatusBroadcastInfo(ctx) wrapped = wa.wrapStatusBroadcastInfo(ctx)
} else { } else {
return nil, ErrBroadcastList return nil, fmt.Errorf("broadcast list bridging is currently not supported")
} }
case types.GroupServer: case types.GroupServer:
info, err := wa.Client.GetGroupInfo(ctx, portalJID) info, err := wa.Client.GetGroupInfo(ctx, portalJID)
@ -57,8 +50,6 @@ func (wa *WhatsAppClient) getChatInfo(ctx context.Context, portalJID types.JID,
info, err := wa.Client.GetNewsletterInfo(ctx, portalJID) info, err := wa.Client.GetNewsletterInfo(ctx, portalJID)
if err != nil { if err != nil {
return nil, err return nil, err
} else if info == nil || info.ID.IsEmpty() {
return nil, ErrNewsletterNotFound
} }
wrapped = wa.wrapNewsletterInfo(ctx, info) wrapped = wa.wrapNewsletterInfo(ctx, info)
default: default:
@ -108,28 +99,9 @@ func (wa *WhatsAppClient) applyChatSettings(ctx context.Context, chatID types.JI
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get chat settings") zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get chat settings")
return return
} }
if !chat.Found { info.UserLocal = &bridgev2.UserLocalPortalInfo{
chatID, err = wa.GetStore().GetAltJID(ctx, chatID) MutedUntil: ptr.Ptr(chat.MutedUntil),
if err != nil {
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get alternate JID to get chat settings")
return
} }
chat, err = wa.GetStore().ChatSettings.GetChatSettings(ctx, chatID)
if err != nil {
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get chat settings with alternate JID")
return
}
}
wa.appStateRecoveryLock.Lock()
_, muteStateBroken := wa.UserLogin.Metadata.(*waid.UserLoginMetadata).AppStateRecoveryAttempted[appstate.WAPatchRegularHigh]
wa.appStateRecoveryLock.Unlock()
userLocal := &bridgev2.UserLocalPortalInfo{}
if !muteStateBroken {
userLocal.MutedUntil = ptr.Ptr(chat.MutedUntil)
} else if info.UserLocal != nil {
userLocal.MutedUntil = info.UserLocal.MutedUntil
}
info.UserLocal = userLocal
if chat.Pinned { if chat.Pinned {
info.UserLocal.Tag = ptr.Ptr(wa.Main.Config.PinnedTag) info.UserLocal.Tag = ptr.Ptr(wa.Main.Config.PinnedTag)
} else if chat.Archived { } else if chat.Archived {
@ -169,12 +141,7 @@ const PrivateChatTopic = "WhatsApp private chat"
const BotChatTopic = "WhatsApp chat with a bot" const BotChatTopic = "WhatsApp chat with a bot"
func (wa *WhatsAppClient) wrapDMInfo(ctx context.Context, jid types.JID) *bridgev2.ChatInfo { func (wa *WhatsAppClient) wrapDMInfo(ctx context.Context, jid types.JID) *bridgev2.ChatInfo {
ownID := wa.JID
if jid.Server == types.HiddenUserServer {
ownID = wa.GetLID()
}
info := &bridgev2.ChatInfo{ info := &bridgev2.ChatInfo{
Type: ptr.Ptr(database.RoomTypeDM),
Topic: ptr.Ptr(PrivateChatTopic), Topic: ptr.Ptr(PrivateChatTopic),
Members: &bridgev2.ChatMemberList{ Members: &bridgev2.ChatMemberList{
IsFull: true, IsFull: true,
@ -182,7 +149,7 @@ func (wa *WhatsAppClient) wrapDMInfo(ctx context.Context, jid types.JID) *bridge
OtherUserID: waid.MakeUserID(jid), OtherUserID: waid.MakeUserID(jid),
MemberMap: map[networkid.UserID]bridgev2.ChatMember{ MemberMap: map[networkid.UserID]bridgev2.ChatMember{
waid.MakeUserID(jid): {EventSender: wa.makeEventSender(ctx, jid)}, waid.MakeUserID(jid): {EventSender: wa.makeEventSender(ctx, jid)},
waid.MakeUserID(ownID): {EventSender: wa.makeEventSender(ctx, ownID)}, waid.MakeUserID(wa.JID): {EventSender: wa.makeEventSender(ctx, wa.JID)},
}, },
PowerLevels: &bridgev2.PowerLevelOverrides{ PowerLevels: &bridgev2.PowerLevelOverrides{
Events: map[event.Type]int{ Events: map[event.Type]int{
@ -192,19 +159,13 @@ func (wa *WhatsAppClient) wrapDMInfo(ctx context.Context, jid types.JID) *bridge
event.StateBeeperDisappearingTimer: 0, event.StateBeeperDisappearingTimer: 0,
}, },
}, },
ExcludeChangesFromTimeline: true,
}, },
ExcludeChangesFromTimeline: true, Type: ptr.Ptr(database.RoomTypeDM),
}
var err error
info.UserBlocked, err = wa.isUserBlocked(ctx, jid)
if err != nil {
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get blocked state")
} }
if jid.Server == types.BotServer { if jid.Server == types.BotServer {
info.Topic = ptr.Ptr(BotChatTopic) info.Topic = ptr.Ptr(BotChatTopic)
} }
if wa.IsOwnJID(jid) { if jid == wa.JID.ToNonAD() {
// For chats with self, force-split the members so the user's own ghost is always in the room. // For chats with self, force-split the members so the user's own ghost is always in the room.
info.Members.MemberMap = map[networkid.UserID]bridgev2.ChatMember{ info.Members.MemberMap = map[networkid.UserID]bridgev2.ChatMember{
waid.MakeUserID(jid): {EventSender: bridgev2.EventSender{Sender: waid.MakeUserID(jid)}}, waid.MakeUserID(jid): {EventSender: bridgev2.EventSender{Sender: waid.MakeUserID(jid)}},
@ -228,7 +189,7 @@ func (wa *WhatsAppClient) wrapStatusBroadcastInfo(ctx context.Context) *bridgev2
Members: &bridgev2.ChatMemberList{ Members: &bridgev2.ChatMemberList{
IsFull: false, IsFull: false,
MemberMap: map[networkid.UserID]bridgev2.ChatMember{ MemberMap: map[networkid.UserID]bridgev2.ChatMember{
waid.MakeUserID(wa.GetLID()): {EventSender: wa.makeEventSender(ctx, wa.GetLID())}, waid.MakeUserID(wa.JID): {EventSender: wa.makeEventSender(ctx, wa.JID)},
}, },
}, },
Type: ptr.Ptr(database.RoomTypeDefault), Type: ptr.Ptr(database.RoomTypeDefault),
@ -279,8 +240,6 @@ func setTopicID(id, topic string) bridgev2.ExtraUpdater[*bridgev2.Portal] {
func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupInfo) *bridgev2.ChatInfo { func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupInfo) *bridgev2.ChatInfo {
sendEventPL := defaultPL sendEventPL := defaultPL
// Community announcement groups allow non-admins to send threaded replies.
// Power levels can't represent that, but the power level is checked separately when handling messages.
if info.IsAnnounce && !info.IsDefaultSubGroup { if info.IsAnnounce && !info.IsDefaultSubGroup {
sendEventPL = adminPL sendEventPL = adminPL
} }
@ -294,12 +253,11 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
setAddressingMode(info.AddressingMode), setAddressingMode(info.AddressingMode),
setTopicID(info.TopicID, info.Topic), setTopicID(info.TopicID, info.Topic),
) )
syncAllMembers := wa.Main.Config.MaxMemberSync < 0 || len(info.Participants) < wa.Main.Config.MaxMemberSync
wrapped := &bridgev2.ChatInfo{ wrapped := &bridgev2.ChatInfo{
Name: ptr.Ptr(info.Name), Name: ptr.Ptr(info.Name),
Topic: ptr.Ptr(info.Topic), Topic: ptr.Ptr(info.Topic),
Members: &bridgev2.ChatMemberList{ Members: &bridgev2.ChatMemberList{
IsFull: !info.IsIncognito && !info.IsParent && syncAllMembers, IsFull: !info.IsIncognito,
TotalMemberCount: len(info.Participants), TotalMemberCount: len(info.Participants),
MemberMap: make(map[networkid.UserID]bridgev2.ChatMember, len(info.Participants)), MemberMap: make(map[networkid.UserID]bridgev2.ChatMember, len(info.Participants)),
PowerLevels: &bridgev2.PowerLevelOverrides{ PowerLevels: &bridgev2.PowerLevelOverrides{
@ -311,11 +269,11 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
event.StateRoomName: metaChangePL, event.StateRoomName: metaChangePL,
event.StateRoomAvatar: metaChangePL, event.StateRoomAvatar: metaChangePL,
event.StateTopic: metaChangePL, event.StateTopic: metaChangePL,
event.EventEncrypted: defaultPL, // Need to always allow encrypted events for poll responses
event.EventReaction: defaultPL, event.EventReaction: defaultPL,
event.EventRedaction: defaultPL, event.EventRedaction: defaultPL,
event.EventUnstablePollResponse: defaultPL,
event.StateBeeperDisappearingTimer: metaChangePL, event.StateBeeperDisappearingTimer: metaChangePL,
// TODO always allow poll responses
}, },
}, },
}, },
@ -326,42 +284,32 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
}, },
ExtraUpdates: extraUpdater, ExtraUpdates: extraUpdater,
} }
var hasSelf bool
for _, pcp := range info.Participants { for _, pcp := range info.Participants {
member := bridgev2.ChatMember{ member := bridgev2.ChatMember{
EventSender: wa.makeEventSender(ctx, pcp.JID), EventSender: wa.makeEventSender(ctx, pcp.JID),
Membership: event.MembershipJoin, Membership: event.MembershipJoin,
} }
if member.EventSender.IsFromMe {
hasSelf = true
}
if pcp.IsSuperAdmin { if pcp.IsSuperAdmin {
member.PowerLevel = ptr.Ptr(superAdminPL) member.PowerLevel = ptr.Ptr(superAdminPL)
} else if pcp.IsAdmin { } else if pcp.IsAdmin {
member.PowerLevel = ptr.Ptr(adminPL) member.PowerLevel = ptr.Ptr(adminPL)
} else { } else {
if !syncAllMembers && !member.EventSender.IsFromMe {
continue
}
member.PowerLevel = ptr.Ptr(defaultPL) member.PowerLevel = ptr.Ptr(defaultPL)
} }
member.MemberEventExtra = map[string]any{ member.MemberEventExtra = map[string]any{
"com.beeper.exclude_from_timeline": true, "com.beeper.exclude_from_timeline": true,
} }
wrapped.Members.MemberMap.Set(member) wrapped.Members.MemberMap[waid.MakeUserID(pcp.JID)] = member
if pcp.JID.Server == types.HiddenUserServer && !pcp.PhoneNumber.IsEmpty() { if pcp.JID.Server == types.HiddenUserServer && !pcp.PhoneNumber.IsEmpty() {
wrapped.Members.MemberMap.Add(bridgev2.ChatMember{ wrapped.Members.MemberMap[waid.MakeUserID(pcp.PhoneNumber)] = bridgev2.ChatMember{
EventSender: bridgev2.EventSender{Sender: waid.MakeUserID(pcp.PhoneNumber)}, EventSender: bridgev2.EventSender{Sender: waid.MakeUserID(pcp.PhoneNumber)},
Membership: event.MembershipLeave, Membership: event.MembershipLeave,
PrevMembership: event.MembershipJoin, PrevMembership: event.MembershipJoin,
MemberEventExtra: map[string]any{ MemberEventExtra: map[string]any{
"com.beeper.exclude_from_timeline": true, "com.beeper.exclude_from_timeline": true,
}, },
})
} }
} }
if info.IsParent && !hasSelf && info.AddressingMode == types.AddressingModeLID {
wrapped.Members.MemberMap.Add(bridgev2.ChatMember{EventSender: wa.makeEventSender(ctx, wa.GetLID())})
} }
if !info.LinkedParentJID.IsEmpty() { if !info.LinkedParentJID.IsEmpty() {
@ -502,7 +450,7 @@ func (wa *WhatsAppClient) makePortalAvatarFetcher(avatarID string, sender types.
wrappedAvatar = &bridgev2.Avatar{ wrappedAvatar = &bridgev2.Avatar{
ID: networkid.AvatarID(avatar.ID), ID: networkid.AvatarID(avatar.ID),
Get: func(ctx context.Context) ([]byte, error) { Get: func(ctx context.Context) ([]byte, error) {
return wa.Client.DownloadMediaWithOnlyPath(ctx, avatar.DirectPath) return wa.Client.DownloadMediaWithPath(ctx, avatar.DirectPath, nil, nil, nil, 0, "", "")
}, },
} }
} }
@ -541,7 +489,7 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne
if info.ThreadMeta.Picture != nil { if info.ThreadMeta.Picture != nil {
avatar.ID = networkid.AvatarID(info.ThreadMeta.Picture.ID) avatar.ID = networkid.AvatarID(info.ThreadMeta.Picture.ID)
avatar.Get = func(ctx context.Context) ([]byte, error) { avatar.Get = func(ctx context.Context) ([]byte, error) {
return wa.Client.DownloadMediaWithOnlyPath(ctx, info.ThreadMeta.Picture.DirectPath) return wa.Client.DownloadMediaWithPath(ctx, info.ThreadMeta.Picture.DirectPath, nil, nil, nil, 0, "", "")
} }
} else if info.ThreadMeta.Preview.ID != "" { } else if info.ThreadMeta.Preview.ID != "" {
avatar.ID = networkid.AvatarID(info.ThreadMeta.Preview.ID) avatar.ID = networkid.AvatarID(info.ThreadMeta.Preview.ID)
@ -552,7 +500,7 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne
} else if meta.ThreadMeta.Picture == nil { } else if meta.ThreadMeta.Picture == nil {
return nil, fmt.Errorf("full res avatar info is missing") return nil, fmt.Errorf("full res avatar info is missing")
} }
return wa.Client.DownloadMediaWithOnlyPath(ctx, meta.ThreadMeta.Picture.DirectPath) return wa.Client.DownloadMediaWithPath(ctx, meta.ThreadMeta.Picture.DirectPath, nil, nil, nil, 0, "", "")
} }
} else { } else {
avatar.ID = "remove" avatar.ID = "remove"
@ -568,8 +516,8 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne
Members: &bridgev2.ChatMemberList{ Members: &bridgev2.ChatMemberList{
TotalMemberCount: info.ThreadMeta.SubscriberCount, TotalMemberCount: info.ThreadMeta.SubscriberCount,
MemberMap: map[networkid.UserID]bridgev2.ChatMember{ MemberMap: map[networkid.UserID]bridgev2.ChatMember{
waid.MakeUserID(wa.GetLID()): { waid.MakeUserID(wa.JID): {
EventSender: wa.makeEventSender(ctx, wa.GetLID()), EventSender: wa.makeEventSender(ctx, wa.JID),
PowerLevel: &ownPowerLevel, PowerLevel: &ownPowerLevel,
}, },
}, },
@ -583,27 +531,10 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne
event.StateTopic: adminPL, event.StateTopic: adminPL,
event.EventReaction: defaultPL, event.EventReaction: defaultPL,
event.EventRedaction: defaultPL, event.EventRedaction: defaultPL,
// TODO support poll votes in channels // TODO always allow poll responses
//event.EventUnstablePollResponse: defaultPL,
}, },
}, },
}, },
Type: ptr.Ptr(database.RoomTypeDefault), Type: ptr.Ptr(database.RoomTypeDefault),
} }
} }
func (wa *WhatsAppClient) isUserBlocked(ctx context.Context, jid types.JID) (*bool, error) {
wa.blocklistLock.Lock()
defer wa.blocklistLock.Unlock()
meta := wa.UserLogin.Metadata.(*waid.UserLoginMetadata)
blocklist := meta.Blocklist
if blocklist == nil {
runHandlers := wa.resyncBlocklistUnlocked(ctx)
go runHandlers()
blocklist = meta.Blocklist
}
if blocklist == nil {
return nil, nil
}
return new(blocklist.IsBlocked(jid)), nil
}

View file

@ -38,7 +38,6 @@ import (
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/bridgev2/status" "maunium.net/go/mautrix/bridgev2/status"
"maunium.net/go/mautrix/event"
"go.mau.fi/mautrix-whatsapp/pkg/waid" "go.mau.fi/mautrix-whatsapp/pkg/waid"
) )
@ -54,12 +53,8 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2.
directMediaRetries: make(map[networkid.MessageID]*directMediaRetry), directMediaRetries: make(map[networkid.MessageID]*directMediaRetry),
mediaRetryLock: semaphore.NewWeighted(wa.Config.HistorySync.MediaRequests.MaxAsyncHandle), mediaRetryLock: semaphore.NewWeighted(wa.Config.HistorySync.MediaRequests.MaxAsyncHandle),
pushNamesSynced: exsync.NewEvent(), pushNamesSynced: exsync.NewEvent(),
createDedup: exsync.NewSet[types.MessageID](),
appStateFullSyncAttempted: make(map[appstate.WAPatchName]time.Time), appStateFullSyncAttempted: make(map[appstate.WAPatchName]time.Time),
previousOwnLIDs: exsync.NewSetWithItems(login.Metadata.(*waid.UserLoginMetadata).PreviousOwnLIDs),
onDemandHistorySyncRequests: make(map[types.JID]onDemandHistorySyncRequest),
disableNewsletter: store.BaseClientPayload.GetUserAgent().GetPlatform() == waWa6.ClientPayload_UserAgent_MACOS,
} }
login.Client = w login.Client = w
@ -74,18 +69,16 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2.
if err != nil { if err != nil {
return err return err
} }
w.LID = w.Device.GetLID()
if w.Device != nil { if w.Device != nil {
log := w.UserLogin.Log.With().Str("component", "whatsmeow").Logger() log := w.UserLogin.Log.With().Str("component", "whatsmeow").Logger()
w.Client = whatsmeow.NewClient(w.Device, waLog.Zerolog(log)) w.Client = whatsmeow.NewClient(w.Device, waLog.Zerolog(log))
w.Client.AddEventHandlerWithSuccessStatus(w.handleWAEvent) w.Client.AddEventHandlerWithSuccessStatus(w.handleWAEvent)
w.Client.SynchronousAck = true w.Client.SynchronousAck = true
w.Client.EnableDecryptedEventBuffer = wa.Bridge.Config.PortalEventBuffer == 0 w.Client.EnableDecryptedEventBuffer = bridgev2.PortalEventBuffer == 0
w.Client.ManualHistorySyncDownload = true w.Client.ManualHistorySyncDownload = true
w.Client.SendReportingTokens = true w.Client.SendReportingTokens = true
w.Client.AutomaticMessageRerequestFromPhone = true w.Client.AutomaticMessageRerequestFromPhone = true
w.Client.UnifiedSessionInterval = 24 * time.Hour
w.Client.GetMessageForRetry = w.trackNotFoundRetry w.Client.GetMessageForRetry = w.trackNotFoundRetry
w.Client.PreRetryCallback = w.trackFoundRetry w.Client.PreRetryCallback = w.trackFoundRetry
w.Client.BackgroundEventCtx = w.UserLogin.Log.WithContext(wa.Bridge.BackgroundCtx) w.Client.BackgroundEventCtx = w.UserLogin.Log.WithContext(wa.Bridge.BackgroundCtx)
@ -110,7 +103,6 @@ type WhatsAppClient struct {
Client *whatsmeow.Client Client *whatsmeow.Client
Device *store.Device Device *store.Device
JID types.JID JID types.JID
LID types.JID
MC mClient MC mClient
historySyncWakeup chan struct{} historySyncWakeup chan struct{}
@ -120,28 +112,15 @@ type WhatsAppClient struct {
nextResync time.Time nextResync time.Time
directMediaRetries map[networkid.MessageID]*directMediaRetry directMediaRetries map[networkid.MessageID]*directMediaRetry
directMediaLock sync.Mutex directMediaLock sync.Mutex
avatarLock exsync.KeyedMutex[types.JID]
mediaRetryLock *semaphore.Weighted mediaRetryLock *semaphore.Weighted
offlineSyncWaiter atomic.Pointer[chan error] offlineSyncWaiter atomic.Pointer[chan error]
isNewLogin bool isNewLogin bool
pushNamesSynced *exsync.Event pushNamesSynced *exsync.Event
lastPresence types.Presence lastPresence types.Presence
blocklistLock sync.Mutex createDedup *exsync.Set[types.MessageID]
disableNewsletter bool
appStateRecoveryLock sync.Mutex appStateRecoveryLock sync.Mutex
appStateFullSyncAttempted map[appstate.WAPatchName]time.Time appStateFullSyncAttempted map[appstate.WAPatchName]time.Time
wasaResyncLock sync.Mutex
museProfileLock sync.Mutex
lastMuseProfileRequest time.Time
onDemandHistorySyncLock sync.Mutex
onDemandHistorySyncRequests map[types.JID]onDemandHistorySyncRequest
previousOwnLIDs *exsync.Set[types.JID]
previousOwnLIDsLock sync.Mutex
} }
var ( var (
@ -149,7 +128,6 @@ var (
_ bridgev2.PushableNetworkAPI = (*WhatsAppClient)(nil) _ bridgev2.PushableNetworkAPI = (*WhatsAppClient)(nil)
_ bridgev2.BackgroundSyncingNetworkAPI = (*WhatsAppClient)(nil) _ bridgev2.BackgroundSyncingNetworkAPI = (*WhatsAppClient)(nil)
_ bridgev2.ChatViewingNetworkAPI = (*WhatsAppClient)(nil) _ bridgev2.ChatViewingNetworkAPI = (*WhatsAppClient)(nil)
_ bridgev2.StickerImportingNetworkAPI = (*WhatsAppClient)(nil)
) )
var pushCfg = &bridgev2.PushConfig{ var pushCfg = &bridgev2.PushConfig{
@ -205,52 +183,17 @@ func (wa *WhatsAppClient) RegisterPushNotifications(ctx context.Context, pushTyp
} }
func (wa *WhatsAppClient) IsThisUser(_ context.Context, userID networkid.UserID) bool { func (wa *WhatsAppClient) IsThisUser(_ context.Context, userID networkid.UserID) bool {
return userID == waid.MakeUserID(wa.JID) || userID == waid.MakeUserID(wa.GetLID()) return userID == waid.MakeUserID(wa.JID)
}
func (wa *WhatsAppClient) IsOwnJID(jid types.JID) bool {
return (jid.Server == types.DefaultUserServer && jid.User == wa.JID.User) ||
(jid.Server == types.HiddenUserServer && jid.User == wa.GetLID().User) ||
wa.previousOwnLIDs.Has(jid.ToNonAD())
}
func (wa *WhatsAppClient) addPreviousOwnLID(ctx context.Context, jid types.JID) {
if jid.Server != types.HiddenUserServer || wa.IsOwnJID(jid) {
return
}
jid = jid.ToNonAD()
wa.previousOwnLIDsLock.Lock()
defer wa.previousOwnLIDsLock.Unlock()
if !wa.previousOwnLIDs.Add(jid) {
return
}
zerolog.Ctx(ctx).Info().Stringer("lid", jid).Msg("Found previous own LID in history sync")
wa.UserLogin.Metadata.(*waid.UserLoginMetadata).PreviousOwnLIDs = wa.previousOwnLIDs.AsList()
err := wa.UserLogin.Save(ctx)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to save user login after adding previous own LID")
}
}
func (wa *WhatsAppClient) GetLID() types.JID {
if wa.LID.IsEmpty() && !wa.JID.IsEmpty() {
wa.LID = wa.GetStore().GetLID()
}
return wa.LID
} }
func (wa *WhatsAppClient) Connect(ctx context.Context) { func (wa *WhatsAppClient) Connect(ctx context.Context) {
wa.connect(ctx)
}
func (wa *WhatsAppClient) connect(ctx context.Context) bool {
if wa.Client == nil { if wa.Client == nil {
state := status.BridgeState{ state := status.BridgeState{
StateEvent: status.StateBadCredentials, StateEvent: status.StateBadCredentials,
Error: WANotLoggedIn, Error: WANotLoggedIn,
} }
wa.UserLogin.BridgeState.Send(state) wa.UserLogin.BridgeState.Send(state)
return false return
} }
wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnecting}) wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnecting})
wa.Main.firstClientConnectOnce.Do(wa.Main.onFirstClientConnect) wa.Main.firstClientConnectOnce.Do(wa.Main.onFirstClientConnect)
@ -258,7 +201,7 @@ func (wa *WhatsAppClient) connect(ctx context.Context) bool {
zerolog.Ctx(ctx).Err(err).Msg("Failed to update proxy") zerolog.Ctx(ctx).Err(err).Msg("Failed to update proxy")
} }
if ctx.Err() != nil { if ctx.Err() != nil {
return false return
} }
wa.initMC() wa.initMC()
wa.startLoops() wa.startLoops()
@ -275,9 +218,7 @@ func (wa *WhatsAppClient) connect(ctx context.Context) bool {
}, },
} }
wa.UserLogin.BridgeState.Send(state) wa.UserLogin.BridgeState.Send(state)
return false
} }
return true
} }
func (wa *WhatsAppClient) notifyOfflineSyncWaiter(err error) { func (wa *WhatsAppClient) notifyOfflineSyncWaiter(err error) {
@ -434,10 +375,6 @@ func (wa *WhatsAppClient) LogoutRemote(ctx context.Context) {
} }
wa.Disconnect() wa.Disconnect()
wa.Client = nil wa.Client = nil
err := wa.Main.DB.Conversation.DeleteAll(ctx, wa.UserLogin.ID)
if err != nil {
zerolog.Ctx(ctx).Err(err).Msg("Failed to delete history sync data on logout")
}
} }
func (wa *WhatsAppClient) IsLoggedIn() bool { func (wa *WhatsAppClient) IsLoggedIn() bool {
@ -494,10 +431,6 @@ func (wa *WhatsAppClient) HandleMatrixViewingChat(ctx context.Context, msg *brid
} }
} }
if msg.Portal != nil && msg.Portal.ID == waid.MakePortalID(types.MuseJID) {
go wa.requestMuseProfile(ctx)
}
if msg.Portal == nil || msg.Portal.Metadata.(*waid.PortalMetadata).LastSync.Add(5*time.Minute).After(time.Now()) { if msg.Portal == nil || msg.Portal.Metadata.(*waid.PortalMetadata).LastSync.Add(5*time.Minute).After(time.Now()) {
// If we resynced this portal within the last 5 minutes, don't do it again // If we resynced this portal within the last 5 minutes, don't do it again
return nil return nil
@ -534,12 +467,3 @@ func (wa *WhatsAppClient) updatePresence(ctx context.Context, presence types.Pre
} }
return err return err
} }
func (wa *WhatsAppClient) DownloadImagePack(ctx context.Context, url string) (*bridgev2.ImportedImagePack, error) {
return wa.Main.MsgConv.DownloadImagePack(ctx, wa.UserLogin.ID, wa.Client, url)
}
func (wa *WhatsAppClient) ListImagePacks(ctx context.Context) ([]*event.ImagePackMetadata, error) {
// TODO
return nil, nil
}

View file

@ -148,7 +148,13 @@ func fnSync(ce *commands.Event) {
case "appstate": case "appstate":
names := appstate.AllPatchNames[:] names := appstate.AllPatchNames[:]
if len(ce.Args) > 1 { if len(ce.Args) > 1 {
names = parseAppStateNames(ce, ce.Args[1:]) names = exslices.CastFuncFilter(ce.Args[1:], func(name string) (appstate.WAPatchName, bool) {
if !slices.Contains(appstate.AllPatchNames[:], appstate.WAPatchName(name)) {
ce.Reply("Invalid app state name `%s`", name)
return "", false
}
return appstate.WAPatchName(name), true
})
} }
for _, name := range names { for _, name := range names {
err := wa.Client.FetchAppState(ce.Ctx, name, true, false) err := wa.Client.FetchAppState(ce.Ctx, name, true, false)
@ -168,50 +174,6 @@ func fnSync(ce *commands.Event) {
} }
} }
func parseAppStateNames(ce *commands.Event, args []string) []appstate.WAPatchName {
return exslices.CastFuncFilter(args, func(name string) (appstate.WAPatchName, bool) {
if !slices.Contains(appstate.AllPatchNames[:], appstate.WAPatchName(name)) {
ce.Reply("Invalid app state name `%s`", name)
return "", false
}
return appstate.WAPatchName(name), true
})
}
var cmdResetAppState = &commands.FullHandler{
Func: fnResetAppState,
Name: "reset-appstate",
Help: commands.HelpMeta{
Section: commands.HelpSectionAdmin,
Description: "Ask your phone to reset broken app state. This logs out all linked devices.",
Args: "<names...>",
},
RequiresLogin: true,
}
func fnResetAppState(ce *commands.Event) {
login := ce.User.GetDefaultLogin()
if login == nil {
ce.Reply("Login not found")
return
}
if len(ce.Args) == 0 {
ce.Reply("Usage: `$cmdprefix reset-appstate <names...>`\n\nThis logs out all linked devices, including the bridge. You'll have to log in again afterwards.")
return
}
names := parseAppStateNames(ce, ce.Args)
if len(names) != len(ce.Args) {
return
}
wa := login.Client.(*WhatsAppClient)
_, err := wa.Client.SendPeerMessage(ce.Ctx, whatsmeow.BuildFatalAppStateExceptionNotification(names...))
if err != nil {
ce.Reply("Failed to send app state reset request: %v", err)
return
}
ce.Reply("Sent app state reset request. Your phone should log out all linked devices shortly, after which you can log in again.")
}
var cmdInviteLink = &commands.FullHandler{ var cmdInviteLink = &commands.FullHandler{
Func: fnInviteLink, Func: fnInviteLink,
Name: "invite-link", Name: "invite-link",

View file

@ -2,7 +2,6 @@ package connector
import ( import (
_ "embed" _ "embed"
"fmt"
"strings" "strings"
"text/template" "text/template"
"time" "time"
@ -52,8 +51,6 @@ type Config struct {
DirectMediaAutoRequest bool `yaml:"direct_media_auto_request"` DirectMediaAutoRequest bool `yaml:"direct_media_auto_request"`
InitialAutoReconnect bool `yaml:"initial_auto_reconnect"` InitialAutoReconnect bool `yaml:"initial_auto_reconnect"`
UseWhatsAppRetryStore bool `yaml:"use_whatsapp_retry_store"` UseWhatsAppRetryStore bool `yaml:"use_whatsapp_retry_store"`
MaxMemberSync int `yaml:"max_member_sync"`
LazyAvatars bool `yaml:"lazy_avatars"`
AnimatedSticker msgconv.AnimatedStickerConfig `yaml:"animated_sticker"` AnimatedSticker msgconv.AnimatedStickerConfig `yaml:"animated_sticker"`
@ -73,8 +70,6 @@ type Config struct {
RequestLocalTime int `yaml:"request_local_time"` RequestLocalTime int `yaml:"request_local_time"`
MaxAsyncHandle int64 `yaml:"max_async_handle"` MaxAsyncHandle int64 `yaml:"max_async_handle"`
} `yaml:"media_requests"` } `yaml:"media_requests"`
BackwardsOnDemand bool `yaml:"backwards_on_demand"`
} `yaml:"history_sync"` } `yaml:"history_sync"`
displaynameTemplate *template.Template `yaml:"-"` displaynameTemplate *template.Template `yaml:"-"`
@ -93,16 +88,8 @@ func (c *Config) UnmarshalYAML(node *yaml.Node) error {
func (c *Config) PostProcess() error { func (c *Config) PostProcess() error {
var err error var err error
c.displaynameTemplate, err = template.New("displayname").Parse(c.DisplaynameTemplate) c.displaynameTemplate, err = template.New("displayname").Parse(c.DisplaynameTemplate)
if err != nil {
return err return err
} }
// Try to execute template to make sure it's valid
_, err = c.formatDisplayname(types.PSAJID, "", types.ContactInfo{})
if err != nil {
return fmt.Errorf("failed to execute displayname template: %w", err)
}
return nil
}
func upgradeConfig(helper up.Helper) { func upgradeConfig(helper up.Helper) {
helper.Copy(up.Str, "os_name") helper.Copy(up.Str, "os_name")
@ -131,8 +118,6 @@ func upgradeConfig(helper up.Helper) {
helper.Copy(up.Bool, "direct_media_auto_request") helper.Copy(up.Bool, "direct_media_auto_request")
helper.Copy(up.Bool, "initial_auto_reconnect") helper.Copy(up.Bool, "initial_auto_reconnect")
helper.Copy(up.Bool, "use_whatsapp_retry_store") helper.Copy(up.Bool, "use_whatsapp_retry_store")
helper.Copy(up.Int, "max_member_sync")
helper.Copy(up.Bool, "lazy_avatars")
helper.Copy(up.Str, "animated_sticker", "target") helper.Copy(up.Str, "animated_sticker", "target")
helper.Copy(up.Int, "animated_sticker", "args", "width") helper.Copy(up.Int, "animated_sticker", "args", "width")
@ -149,7 +134,6 @@ func upgradeConfig(helper up.Helper) {
helper.Copy(up.Str, "history_sync", "media_requests", "request_method") helper.Copy(up.Str, "history_sync", "media_requests", "request_method")
helper.Copy(up.Int, "history_sync", "media_requests", "request_local_time") helper.Copy(up.Int, "history_sync", "media_requests", "request_local_time")
helper.Copy(up.Int, "history_sync", "media_requests", "max_async_handle") helper.Copy(up.Int, "history_sync", "media_requests", "max_async_handle")
helper.Copy(up.Bool, "history_sync", "backwards_on_demand")
} }
type DisplaynameParams struct { type DisplaynameParams struct {
@ -164,7 +148,7 @@ type DisplaynameParams struct {
Short string Short string
} }
func (c *Config) formatDisplayname(jid types.JID, phone string, contact types.ContactInfo) (string, error) { func (c *Config) FormatDisplayname(jid types.JID, phone string, contact types.ContactInfo) string {
var nameBuf strings.Builder var nameBuf strings.Builder
if phone == "" && jid.Server == types.DefaultUserServer { if phone == "" && jid.Server == types.DefaultUserServer {
phone = "+" + jid.User phone = "+" + jid.User
@ -183,21 +167,13 @@ func (c *Config) formatDisplayname(jid types.JID, phone string, contact types.Co
Name: contact.FullName, Name: contact.FullName,
Short: contact.FirstName, Short: contact.FirstName,
}) })
return nameBuf.String(), err
}
func (c *Config) FormatDisplayname(jid types.JID, phone string, contact types.ContactInfo) string {
name, err := c.formatDisplayname(jid, phone, contact)
if err != nil { if err != nil {
panic(err) panic(err)
} }
return name return nameBuf.String()
} }
func redactPhone(phone string) string { func redactPhone(phone string) string {
if len(phone) <= 4 {
return phone
}
// This doesn't keep 2+ digit country codes properly, but whatever // This doesn't keep 2+ digit country codes properly, but whatever
return phone[:2] + strings.Repeat("∙", len(phone)-4) + phone[len(phone)-2:] return phone[:2] + strings.Repeat("∙", len(phone)-4) + phone[len(phone)-2:]
} }
@ -209,7 +185,6 @@ func (wa *WhatsAppConnector) GetConfig() (string, any, up.Upgrader) {
{"proxy"}, {"proxy"},
{"displayname_template"}, {"displayname_template"},
{"call_start_notices"}, {"call_start_notices"},
{"animated_sticker"},
{"history_sync"}, {"history_sync"},
}, },
Base: ExampleConfig, Base: ExampleConfig,

View file

@ -28,24 +28,27 @@ import (
"time" "time"
"github.com/lib/pq" "github.com/lib/pq"
"github.com/rs/zerolog"
"go.mau.fi/util/dbutil" "go.mau.fi/util/dbutil"
"go.mau.fi/util/exsync"
"go.mau.fi/util/random" "go.mau.fi/util/random"
"go.mau.fi/whatsmeow" "go.mau.fi/whatsmeow"
"go.mau.fi/whatsmeow/proto/waCompanionReg" "go.mau.fi/whatsmeow/proto/waCompanionReg"
"go.mau.fi/whatsmeow/store" "go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/store/sqlstore" "go.mau.fi/whatsmeow/store/sqlstore"
whatsmeowUpgrades "go.mau.fi/whatsmeow/store/sqlstore/upgrades" whatsmeowUpgrades "go.mau.fi/whatsmeow/store/sqlstore/upgrades"
"go.mau.fi/whatsmeow/types"
waLog "go.mau.fi/whatsmeow/util/log" waLog "go.mau.fi/whatsmeow/util/log"
"google.golang.org/protobuf/proto" "google.golang.org/protobuf/proto"
"maunium.net/go/mautrix/bridgev2" "maunium.net/go/mautrix/bridgev2"
"maunium.net/go/mautrix/bridgev2/commands" "maunium.net/go/mautrix/bridgev2/commands"
"maunium.net/go/mautrix/bridgev2/database"
"maunium.net/go/mautrix/bridgev2/networkid" "maunium.net/go/mautrix/bridgev2/networkid"
"maunium.net/go/mautrix/event" "maunium.net/go/mautrix/event"
"maunium.net/go/mautrix/id" "maunium.net/go/mautrix/id"
"go.mau.fi/mautrix-whatsapp/pkg/connector/wadb" "go.mau.fi/mautrix-whatsapp/pkg/connector/wadb"
"go.mau.fi/mautrix-whatsapp/pkg/msgconv" "go.mau.fi/mautrix-whatsapp/pkg/msgconv"
"go.mau.fi/mautrix-whatsapp/pkg/waid"
) )
type WhatsAppConnector struct { type WhatsAppConnector struct {
@ -61,8 +64,6 @@ type WhatsAppConnector struct {
mediaEditCache MediaEditCache mediaEditCache MediaEditCache
mediaEditCacheLock sync.RWMutex mediaEditCacheLock sync.RWMutex
stopMediaEditCacheLoop atomic.Pointer[context.CancelFunc] stopMediaEditCacheLoop atomic.Pointer[context.CancelFunc]
unmigratedDMs *exsync.Set[networkid.PortalKey]
} }
func init() { func init() {
@ -110,10 +111,9 @@ func (wa *WhatsAppConnector) Init(bridge *bridgev2.Bridge) {
wa.DB = wadb.New(bridge.ID, bridge.DB.Database, bridge.Log.With().Str("db_section", "whatsapp").Logger()) wa.DB = wadb.New(bridge.ID, bridge.DB.Database, bridge.Log.With().Str("db_section", "whatsapp").Logger())
wa.MsgConv.DB = wa.DB wa.MsgConv.DB = wa.DB
wa.Bridge.Commands.(*commands.Processor).AddHandlers( wa.Bridge.Commands.(*commands.Processor).AddHandlers(
cmdAccept, cmdSync, cmdResetAppState, cmdInviteLink, cmdResolveLink, cmdJoin, cmdAccept, cmdSync, cmdInviteLink, cmdResolveLink, cmdJoin,
) )
wa.mediaEditCache = make(MediaEditCache) wa.mediaEditCache = make(MediaEditCache)
wa.unmigratedDMs = exsync.NewSet[networkid.PortalKey]()
whatsmeowDBLog := bridge.Log.With().Str("db_section", "whatsmeow").Logger() whatsmeowDBLog := bridge.Log.With().Str("db_section", "whatsmeow").Logger()
wa.DeviceStore = sqlstore.NewWithWrappedDB( wa.DeviceStore = sqlstore.NewWithWrappedDB(
@ -142,9 +142,6 @@ func (wa *WhatsAppConnector) Init(bridge *bridgev2.Bridge) {
} }
func (wa *WhatsAppConnector) Start(ctx context.Context) error { func (wa *WhatsAppConnector) Start(ctx context.Context) error {
if !wa.MsgConv.DirectMedia && wa.Config.LazyAvatars {
return fmt.Errorf("lazy_avatars set without enabling global direct_media")
}
err := wa.DeviceStore.Upgrade(ctx) err := wa.DeviceStore.Upgrade(ctx)
if err != nil { if err != nil {
return bridgev2.DBUpgradeError{Err: err, Section: "whatsmeow"} return bridgev2.DBUpgradeError{Err: err, Section: "whatsmeow"}
@ -160,27 +157,80 @@ func (wa *WhatsAppConnector) Start(ctx context.Context) error {
return bridgev2.DBUpgradeError{Err: err, Section: "whatsapp"} return bridgev2.DBUpgradeError{Err: err, Section: "whatsapp"}
} }
err = wa.migrateToLIDDMs(ctx) if !wa.Bridge.Background && wa.Bridge.DB.KV.Get(ctx, "whatsapp_lid_dms_deleted") == "false" {
if err != nil { wa.deleteLIDDMsMigration(ctx)
return fmt.Errorf("failed to migrate to LID DMs: %w", err)
} }
err = wa.migrateMetaAIPortals(ctx)
if err != nil {
return fmt.Errorf("failed to migrate Meta AI portals: %w", err)
}
go func() {
err = wa.syncMismatchingGhosts(wa.Bridge.BackgroundCtx)
if err != nil {
wa.Bridge.Log.Err(err).Msg("Failed to sync mismatching ghosts")
}
err = wa.removeRejoinedPNGhosts(wa.Bridge.BackgroundCtx)
if err != nil {
wa.Bridge.Log.Err(err).Msg("Failed to remove rejoined PN ghosts")
}
}()
return nil return nil
} }
func (wa *WhatsAppConnector) deleteLIDDMsMigration(ctx context.Context) {
log := zerolog.Ctx(ctx).With().Str("action", "delete lid dms").Logger()
portals, err := wa.Bridge.GetAllPortalsWithMXID(ctx)
if err != nil {
log.Err(err).Msg("Failed to get portals for LID DM deletion")
return
}
defer wa.Bridge.DB.KV.Set(ctx, "whatsapp_lid_dms_deleted", "true")
if len(portals) == 0 {
log.Debug().Msg("No portals found")
return
}
portalsByKey := make(map[networkid.PortalKey]*bridgev2.Portal, len(portals))
for _, p := range portals {
if p.Receiver == "" || p.RoomType != database.RoomTypeDM {
continue
}
portalsByKey[p.PortalKey] = p
}
_, err = wa.DB.Exec(ctx, "DELETE FROM whatsapp_history_sync_conversation WHERE chat_jid LIKE '%@lid'")
if err != nil {
log.Err(err).Msg("Failed to remove LID conversations from history sync")
}
for key, portal := range portalsByKey {
parsedID, err := waid.ParsePortalID(key.ID)
if err != nil {
log.Warn().Err(err).Str("portal_id", string(key.ID)).Msg("Failed to parse portal ID")
continue
} else if parsedID.Server != types.HiddenUserServer {
continue
}
var pnStr string
err = wa.DB.QueryRow(ctx, "SELECT pn FROM whatsmeow_lid_map WHERE lid=$1", parsedID.User).Scan(&pnStr)
if err != nil {
log.Warn().Err(err).Str("portal_id", string(key.ID)).Msg("Failed to get PN for LID portal")
continue
}
key.ID = waid.MakePortalID(types.JID{User: pnStr, Server: types.DefaultUserServer})
_, pnPortalExists := portalsByKey[key]
if !pnPortalExists {
log.Warn().Str("portal_id", string(key.ID)).Msg("PN portal does not exist, not deleting LID DM")
continue
}
err = portal.Delete(ctx)
if err != nil {
log.Err(err).
Object("portal_key", portal.PortalKey).
Stringer("portal_mxid", portal.MXID).
Msg("Failed to delete LID DM portal from database")
continue
}
err = wa.Bridge.Bot.DeleteRoom(ctx, portal.MXID, false)
if err != nil {
log.Err(err).
Object("portal_key", portal.PortalKey).
Stringer("portal_mxid", portal.MXID).
Msg("Failed to delete LID DM portal from Matrix")
continue
}
log.Debug().
Object("portal_key", portal.PortalKey).
Stringer("portal_mxid", portal.MXID).
Msg("Deleted LID DM portal")
}
log.Info().Msg("Finished deleting LID DM portals")
}
func (wa *WhatsAppConnector) Stop() { func (wa *WhatsAppConnector) Stop() {
if stop := wa.stopMediaEditCacheLoop.Swap(nil); stop != nil { if stop := wa.stopMediaEditCacheLoop.Swap(nil); stop != nil {
(*stop)() (*stop)()
@ -243,16 +293,7 @@ func (wa *WhatsAppConnector) GenerateTransactionID(_ id.UserID, _ id.RoomID, _ e
} }
func (wa *WhatsAppConnector) ResetHTTPTransport() { func (wa *WhatsAppConnector) ResetHTTPTransport() {
for _, login := range wa.Bridge.GetAllCachedUserLogins() { // No-op for now, whatsmeow doesn't use the shared transport config yet
client := login.Client.(*WhatsAppClient).Client
if client == nil {
continue
}
err := wa.updateProxy(login.Log.WithContext(wa.Bridge.BackgroundCtx), client, false)
if err != nil {
login.Log.Err(err).Msg("Failed to recreate HTTP clients")
}
}
} }
func (wa *WhatsAppConnector) ResetNetworkConnections() { func (wa *WhatsAppConnector) ResetNetworkConnections() {

Some files were not shown because too many files have changed in this diff Show more