mirror of
https://github.com/mautrix/whatsapp.git
synced 2026-08-22 12:04:55 -04:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a7217e0ff | ||
|
|
7ee5aa647c |
48 changed files with 1030 additions and 2109 deletions
14
.github/ISSUE_TEMPLATE/bug.md
vendored
14
.github/ISSUE_TEMPLATE/bug.md
vendored
|
|
@ -7,12 +7,10 @@ type: Bug
|
|||
|
||||
---
|
||||
|
||||
<!-- Include relevant logs, the bridge version and other important details here -->
|
||||
<!--
|
||||
Remember to include relevant logs, the bridge version and any other details.
|
||||
|
||||
### Checklist
|
||||
|
||||
<!-- All items below are mandatory. Issues not following the rules may be closed without comment. -->
|
||||
|
||||
* [ ] This is an actual bug, not just a setup issue (see the [troubleshooting docs](https://docs.mau.fi/bridges/general/troubleshooting.html) or ask in the Matrix room for setup help).
|
||||
* [ ] I am certain that sufficient information is included. Ask in the Matrix room first if not.
|
||||
* [ ] The bug is still present on the main branch. The `!wa version` command output is: ``
|
||||
It's always best to ask in the Matrix room first, especially if you aren't sure
|
||||
what details are needed. Issues with insufficient detail will likely just be
|
||||
ignored or closed immediately.
|
||||
-->
|
||||
|
|
|
|||
8
.github/workflows/go.yml
vendored
8
.github/workflows/go.yml
vendored
|
|
@ -11,14 +11,14 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go-version: ["1.26", "1.27"]
|
||||
name: Lint ${{ matrix.go-version == '1.27' && '(latest)' || '(old)' }}
|
||||
go-version: ["1.25", "1.26"]
|
||||
name: Lint ${{ matrix.go-version == '1.26' && '(latest)' || '(old)' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v6
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v7
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
cache: true
|
||||
|
|
|
|||
31
CHANGELOG.md
31
CHANGELOG.md
|
|
@ -1,34 +1,3 @@
|
|||
# v26.08
|
||||
|
||||
* Switched direct chats to use LIDs instead of phone numbers.
|
||||
|
||||
# v26.07
|
||||
|
||||
* Updated Docker image to Alpine 3.24.
|
||||
* Added support for the extra passkey authentication step when logging in.
|
||||
* Added support for newer versions of poll messages from WhatsApp.
|
||||
* Fixed poll votes in DMs.
|
||||
|
||||
# v26.06
|
||||
|
||||
* Added placeholder for group message history share notices.
|
||||
* Fixed community spaces not being bridged properly in some cases.
|
||||
* Fixed handling edits of HD media.
|
||||
* Fixed duplicate message when a message from another own device is initially
|
||||
undecryptable and later gets resolved.
|
||||
|
||||
# v26.05
|
||||
|
||||
* Added support for importing sticker packs from WhatsApp.
|
||||
* Added support for WhatsApp's new message edit encryption scheme.
|
||||
|
||||
# v26.04
|
||||
|
||||
* Added support for @room mentions in both directions.
|
||||
* Changed initial backfill to happen even if WhatsApp doesn't send full history.
|
||||
* Fixed panic when handling updates to unknown polls from WhatsApp.
|
||||
* Fixed some background loops not stopping when a user is logged out.
|
||||
|
||||
# v26.03
|
||||
|
||||
* Added option to save outgoing messages in the database to allow encryption
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1-alpine3.24 AS builder
|
||||
FROM golang:1-alpine3.23 AS builder
|
||||
|
||||
RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev
|
||||
|
||||
|
|
@ -6,7 +6,7 @@ COPY . /build
|
|||
WORKDIR /build
|
||||
RUN ./build.sh
|
||||
|
||||
FROM alpine:3.24
|
||||
FROM alpine:3.23
|
||||
|
||||
ENV UID=1337 \
|
||||
GID=1337
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
ARG DOCKER_HUB="docker.io"
|
||||
|
||||
FROM ${DOCKER_HUB}/alpine:3.24
|
||||
FROM ${DOCKER_HUB}/alpine:3.23
|
||||
|
||||
ENV UID=1337 \
|
||||
GID=1337
|
||||
|
|
|
|||
153
cmd/mautrix-whatsapp/legacyprovision.go
Normal file
153
cmd/mautrix-whatsapp/legacyprovision.go
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -18,12 +18,21 @@ var m = mxmain.BridgeMain{
|
|||
Name: "mautrix-whatsapp",
|
||||
URL: "https://github.com/mautrix/whatsapp",
|
||||
Description: "A Matrix-WhatsApp puppeting bridge.",
|
||||
Version: "26.08",
|
||||
Version: "26.03",
|
||||
SemCalVer: true,
|
||||
Connector: &connector.WhatsAppConnector{},
|
||||
}
|
||||
|
||||
func main() {
|
||||
m.PostStart = func() {
|
||||
if m.Matrix.Provisioning != nil {
|
||||
m.Matrix.Provisioning.Router.HandleFunc("GET /v1/contacts", legacyProvContacts)
|
||||
m.Matrix.Provisioning.Router.HandleFunc("GET /v1/resolve_identifier/{number}", legacyProvResolveIdentifier)
|
||||
m.Matrix.Provisioning.Router.HandleFunc("POST /v1/pm/{number}", legacyProvResolveIdentifier)
|
||||
m.Matrix.Provisioning.Router.HandleFunc("POST /v1/debug/appstate/{patch}", provAppStateDebug)
|
||||
m.Matrix.Provisioning.Router.HandleFunc("POST /v1/debug/recover-appstate/{patch}", provRecoverAppStateDebug)
|
||||
}
|
||||
}
|
||||
m.InitVersion(Tag, Commit, BuildTime)
|
||||
m.Run()
|
||||
}
|
||||
|
|
|
|||
24
cmd/mautrix-whatsapp/plugin.go
Normal file
24
cmd/mautrix-whatsapp/plugin.go
Normal 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)
|
||||
}
|
||||
48
go.mod
48
go.mod
|
|
@ -1,53 +1,53 @@
|
|||
module go.mau.fi/mautrix-whatsapp
|
||||
|
||||
go 1.26.0
|
||||
go 1.25.0
|
||||
|
||||
toolchain go1.27.0
|
||||
toolchain go1.26.1
|
||||
|
||||
tool go.mau.fi/util/cmd/maubuild
|
||||
|
||||
require (
|
||||
github.com/lib/pq v1.12.3
|
||||
github.com/rs/zerolog v1.35.1
|
||||
github.com/tidwall/gjson v1.19.0
|
||||
go.mau.fi/util v0.10.1-0.20260820140024-eb612d936fde
|
||||
go.mau.fi/webp v0.3.0
|
||||
go.mau.fi/whatsmeow v0.0.0-20260821141805-33cfac511629
|
||||
golang.org/x/image v0.45.0
|
||||
golang.org/x/net v0.58.0
|
||||
golang.org/x/sync v0.22.0
|
||||
google.golang.org/protobuf v1.36.12
|
||||
github.com/lib/pq v1.11.2
|
||||
github.com/rs/zerolog v1.34.0
|
||||
go.mau.fi/util v0.9.7
|
||||
go.mau.fi/webp v0.2.0
|
||||
go.mau.fi/whatsmeow v0.0.0-20260305215846-fc65416c22c4
|
||||
golang.org/x/image v0.37.0
|
||||
golang.org/x/net v0.52.0
|
||||
golang.org/x/sync v0.20.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
maunium.net/go/mautrix v0.30.1-0.20260820142923-ae00f2c62e1e
|
||||
maunium.net/go/mautrix v0.26.5-0.20260322102453-0c955c396df7
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/beeper/argo-go v1.1.2 // indirect
|
||||
github.com/coder/websocket v1.8.15 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.7.0 // indirect
|
||||
github.com/coder/websocket v1.8.14 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.6.0 // indirect
|
||||
github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.49 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.34 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6 // indirect
|
||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.2.0 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/vektah/gqlparser/v2 v2.5.27 // indirect
|
||||
github.com/yuin/goldmark v1.8.5 // indirect
|
||||
go.mau.fi/libsignal v0.2.2 // indirect
|
||||
github.com/yuin/goldmark v1.7.16 // indirect
|
||||
go.mau.fi/libsignal v0.2.1 // indirect
|
||||
go.mau.fi/zeroconfig v0.2.0 // indirect
|
||||
golang.org/x/crypto v0.55.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 // indirect
|
||||
golang.org/x/mod v0.40.0 // indirect
|
||||
golang.org/x/sys v0.47.0 // indirect
|
||||
golang.org/x/text v0.41.0 // indirect
|
||||
golang.org/x/crypto v0.49.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 // indirect
|
||||
golang.org/x/mod v0.34.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
maunium.net/go/mauflag v1.0.0 // indirect
|
||||
|
|
|
|||
96
go.sum
96
go.sum
|
|
@ -8,15 +8,17 @@ github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNg
|
|||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
||||
github.com/beeper/argo-go v1.1.2 h1:UQI2G8F+NLfGTOmTUI0254pGKx/HUU/etbUGTJv91Fs=
|
||||
github.com/beeper/argo-go v1.1.2/go.mod h1:M+LJAnyowKVQ6Rdj6XYGEn+qcVFkb3R/MUpqkGR0hM4=
|
||||
github.com/coder/websocket v1.8.15 h1:6B2JPeOGlpff2Uz6vOEH1Vzpi0iUz20A+lPVhPHtNUA=
|
||||
github.com/coder/websocket v1.8.15/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
|
||||
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
|
||||
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/coreos/go-systemd/v22 v22.6.0 h1:aGVa/v8B7hpb0TKl0MWoAavPDmHvobFe5R5zn0bCJWo=
|
||||
github.com/coreos/go-systemd/v22 v22.6.0/go.mod h1:iG+pp635Fo7ZmV/j14KUcmEyWF+0X7Lua8rrTWzYgWU=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
|
||||
github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
|
|
@ -28,17 +30,21 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
|
||||
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/lib/pq v1.11.2 h1:x6gxUeu39V0BHZiugWe8LXZYZ+Utk7hSJGThs8sdzfs=
|
||||
github.com/lib/pq v1.11.2/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.49 h1:B8jBHC3xhxZgxztrgruTuLucebnULQnx4W7cF7SAE9w=
|
||||
github.com/mattn/go-sqlite3 v1.14.49/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
|
||||
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b h1:sS7HLzwS+dO+gxATgQfeZDEdUZe2pKAB3nGoUwP5zU0=
|
||||
github.com/petermattis/goid v0.0.0-20260816044145-ed329add6b1b/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||
github.com/mattn/go-sqlite3 v1.14.34 h1:3NtcvcUnFBPsuRcno8pUtupspG/GM+9nZ88zgJcp6Zk=
|
||||
github.com/mattn/go-sqlite3 v1.14.34/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/petermattis/goid v0.0.0-20260226131333-17d1149c6ac6 h1:rh2lKw/P/EqHa724vYH2+VVQ1YnW4u6EOXl0PMAovZE=
|
||||
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/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
|
|
@ -46,8 +52,8 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR
|
|||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
|
||||
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
|
|
@ -55,8 +61,8 @@ github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDq
|
|||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/gjson v1.19.0 h1:xwxm7n691Uf3u5OFjzngavjGTh55KX5q/9w9xHW88JU=
|
||||
github.com/tidwall/gjson v1.19.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
|
||||
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
|
||||
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
|
|
@ -67,37 +73,39 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
|||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=
|
||||
github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
|
||||
github.com/yuin/goldmark v1.8.5 h1:r6N5afV5qj/5S4UTch8agZHJ8UxNCMwX7WjkkJam2NA=
|
||||
github.com/yuin/goldmark v1.8.5/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
go.mau.fi/libsignal v0.2.2 h1:QV+XdzQkm3x3aSG7FcqfGSZuFXz83pRZPBFaPygHbOU=
|
||||
go.mau.fi/libsignal v0.2.2/go.mod h1:CRlIQg2J8uYTfDFvNoO8/KcZjs5cey0vbc6oj/bssY0=
|
||||
go.mau.fi/util v0.10.1-0.20260820140024-eb612d936fde h1:eMHY9dMDkNuDMWhfTbMZHbbsxj7G6mfujjKei1HaFQM=
|
||||
go.mau.fi/util v0.10.1-0.20260820140024-eb612d936fde/go.mod h1:z0ZZNt4hq3FZbUKnunexE/QscCx7VkLvQSvtggc/aE8=
|
||||
go.mau.fi/webp v0.3.0 h1:gVHQZtz21Ziwj+CDuklbX9mqpsnDIFKxs/BJyV7iZzA=
|
||||
go.mau.fi/webp v0.3.0/go.mod h1:rlZFTev+dYxhvk+XNBP/5GcTt4gXmzAB4DU0aGUYIQo=
|
||||
go.mau.fi/whatsmeow v0.0.0-20260821141805-33cfac511629 h1:99p9fTS1G73a1aifCVaV5MX4hDq0lVUKWGoUNoXyE3A=
|
||||
go.mau.fi/whatsmeow v0.0.0-20260821141805-33cfac511629/go.mod h1:aMd13H2xFFGH9cskcvxo4Aae+TmyFN38yw+HvsrpwVg=
|
||||
github.com/yuin/goldmark v1.7.16 h1:n+CJdUxaFMiDUNnWC3dMWCIQJSkxH4uz3ZwQBkAlVNE=
|
||||
github.com/yuin/goldmark v1.7.16/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
go.mau.fi/libsignal v0.2.1 h1:vRZG4EzTn70XY6Oh/pVKrQGuMHBkAWlGRC22/85m9L0=
|
||||
go.mau.fi/libsignal v0.2.1/go.mod h1:iVvjrHyfQqWajOUaMEsIfo3IqgVMrhWcPiiEzk7NgoU=
|
||||
go.mau.fi/util v0.9.7 h1:AWGNbJfz1zRcQOKeOEYhKUG2fT+/26Gy6kyqcH8tnBg=
|
||||
go.mau.fi/util v0.9.7/go.mod h1:5T2f3ZWZFAGgmFwg3dGw7YK6kIsb9lryDzvynoR98pE=
|
||||
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
|
||||
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
|
||||
go.mau.fi/whatsmeow v0.0.0-20260305215846-fc65416c22c4 h1:FGA3NtCVNeCJ+C+KBg1pODsrfxC/trM3RHFWIeY7y4c=
|
||||
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/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
|
||||
golang.org/x/crypto v0.55.0 h1:+KWHjbgOaAQ66dh/YlkZKHlz9ZUlq61AFirAR9ntP8M=
|
||||
golang.org/x/crypto v0.55.0/go.mod h1:uq0V9dE/fzQuJtbnL+2EhWOE63vo164FY8xqEnV9xis=
|
||||
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297 h1:YXnL44eJ77R+ji4/ooy8UsXIhz+lbi2Qgdlc8iRN0gY=
|
||||
golang.org/x/exp v0.0.0-20260813180055-c1d0aacb2297/go.mod h1:Mkmymgv+uMpSQ/XxJ/7GpdrdYoqm3u72jEbpCLiJmNk=
|
||||
golang.org/x/image v0.45.0 h1:FMb1nTbH5H9vF55SriQHgFw5GnNL9Jg6L25BwXKzhB0=
|
||||
golang.org/x/image v0.45.0/go.mod h1:n62x/7RqlwXDvGsSU4u6IUTUf6KghUZ9Bt7cG/T9Fx4=
|
||||
golang.org/x/mod v0.40.0 h1:hUv+3cXcdRHz08UmSiOob7sadHig73uo5bkXxQ/tvUs=
|
||||
golang.org/x/mod v0.40.0/go.mod h1:0/weTWkPWGBikyTWAX3dkjVztMmBA5hM0DH6BElSupE=
|
||||
golang.org/x/net v0.58.0 h1:ynWG7rqYi4ccpTEuPZ2QGWHktVEM9DMCj9yzDE0Q7To=
|
||||
golang.org/x/net v0.58.0/go.mod h1:YwCddHnFlT7eLQqVprV19OnhLGtc5xOKgE0RyqgfWAU=
|
||||
golang.org/x/sync v0.22.0 h1:SZjpbeLmrCk4xhRSZFNZW5gFUeCeFgjekvI/+gfScek=
|
||||
golang.org/x/sync v0.22.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA=
|
||||
golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ=
|
||||
golang.org/x/image v0.37.0 h1:ZiRjArKI8GwxZOoEtUfhrBtaCN+4b/7709dlT6SSnQA=
|
||||
golang.org/x/image v0.37.0/go.mod h1:/3f6vaXC+6CEanU4KJxbcUZyEePbyKbaLoDOe4ehFYY=
|
||||
golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI=
|
||||
golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
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.47.0 h1:o7XGOvZQCADBQQ4Y7VNq2dRWQR7JmOUW8Kxx4ZsNgWs=
|
||||
golang.org/x/sys v0.47.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.41.0 h1:vz/seA0lnX87Othu2f/0L24RcgrXD9/YFTSuGjj3rH8=
|
||||
golang.org/x/text v0.41.0/go.mod h1:jvf1O8ajNzZqhSrQBPbutR/EB83Cc0CFrezNQIwbb5M=
|
||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
||||
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
|
@ -107,5 +115,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
|
||||
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
||||
maunium.net/go/mautrix v0.30.1-0.20260820142923-ae00f2c62e1e h1:ATvERyBIUXJrnL7ezP2xCLLGCNmGClmfzoQq4N3jBcA=
|
||||
maunium.net/go/mautrix v0.30.1-0.20260820142923-ae00f2c62e1e/go.mod h1:Y02sBiAvfEVqK24bwVGCprmLATRZ7prWel3ZpB413e0=
|
||||
maunium.net/go/mautrix v0.26.5-0.20260322102453-0c955c396df7 h1:KUhlBHWGgknqYC2V8di4DFNh73atDtgPlqqO5FoLmPc=
|
||||
maunium.net/go/mautrix v0.26.5-0.20260322102453-0c955c396df7/go.mod h1:YWw8NWTszsbyFAznboicBObwHPgTSLcuTbVX2kY7U2M=
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/exmaps"
|
||||
"go.mau.fi/util/ptr"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/proto/waE2E"
|
||||
|
|
@ -116,30 +115,14 @@ func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context,
|
|||
Uint32("chunk_order", evt.GetChunkOrder()).
|
||||
Uint32("progress", evt.GetProgress()).
|
||||
Logger()
|
||||
log.Debug().
|
||||
Int64("oldest_msg_in_chunk_ts", evt.GetOldestMsgInChunkTimestampSec()).
|
||||
Any("full_request_meta", evt.GetFullHistorySyncOnDemandRequestMetadata()).
|
||||
Any("access_status", evt.GetMessageAccessStatus()).
|
||||
Str("peer_data_request_session_id", evt.GetPeerDataRequestSessionID()).
|
||||
Msg("Downloading history sync")
|
||||
log.Debug().Msg("Downloading history sync")
|
||||
blob, err := wa.Client.DownloadHistorySync(log.WithContext(ctx), evt, true)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to download history sync")
|
||||
return
|
||||
}
|
||||
if blob.GetSyncType() == waHistorySync.HistorySync_ON_DEMAND {
|
||||
wa.handleOnDemandHistorySync(ctx, blob)
|
||||
if err = wa.Main.DB.HSNotif.Delete(ctx, rowid); err != nil {
|
||||
log.Err(err).Msg("Failed to delete queued on-demand history sync notification")
|
||||
} else if err = wa.Client.DeleteMedia(ctx, whatsmeow.MediaHistory, evt.GetDirectPath(), evt.GetFileEncSHA256(), evt.GetEncHandle()); err != nil {
|
||||
log.Err(err).Msg("Failed to delete history sync blob from server")
|
||||
} else {
|
||||
log.Debug().Msg("Finished handling on-demand history sync and deleted history sync blob from server")
|
||||
}
|
||||
return
|
||||
}
|
||||
err = wa.Main.DB.DoTxn(ctx, nil, func(ctx context.Context) (innerErr error) {
|
||||
innerErr = wa.handleWAHistorySync(ctx, evt, blob, true)
|
||||
resetTimer, innerErr = wa.handleWAHistorySync(ctx, evt, blob, true)
|
||||
if innerErr != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -151,16 +134,6 @@ func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context,
|
|||
})
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to store history sync notification data")
|
||||
} else {
|
||||
resetTimer = blob.GetSyncType() == waHistorySync.HistorySync_INITIAL_BOOTSTRAP ||
|
||||
blob.GetSyncType() == waHistorySync.HistorySync_RECENT ||
|
||||
blob.GetSyncType() == waHistorySync.HistorySync_FULL
|
||||
err = wa.Client.DeleteMedia(ctx, whatsmeow.MediaHistory, evt.GetDirectPath(), evt.GetFileEncSHA256(), evt.GetEncHandle())
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to delete history sync blob from server")
|
||||
} else {
|
||||
log.Debug().Msg("Deleted history sync blob from server")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
|
@ -170,9 +143,9 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
|||
notif *waE2E.HistorySyncNotification,
|
||||
evt *waHistorySync.HistorySync,
|
||||
stopOnError bool,
|
||||
) error {
|
||||
) (bool, error) {
|
||||
if evt == nil || evt.SyncType == nil {
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
log := wa.UserLogin.Log.With().
|
||||
Str("action", "store history sync").
|
||||
|
|
@ -197,7 +170,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
|||
Int("recent_sticker_count", len(evt.GetRecentStickers())).
|
||||
Int("past_participant_count", len(evt.GetPastParticipants())).
|
||||
Msg("Ignoring history sync")
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
log.Info().
|
||||
Int("conversation_count", len(evt.GetConversations())).
|
||||
|
|
@ -205,8 +178,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
|||
Dict("notification_metadata", zerolog.Dict().
|
||||
Int64("oldest_msg_in_chunk_ts", notif.GetOldestMsgInChunkTimestampSec()).
|
||||
Any("full_request_meta", notif.GetFullHistorySyncOnDemandRequestMetadata()).
|
||||
Any("access_status", notif.GetMessageAccessStatus()).
|
||||
Str("peer_data_request_session_id", notif.GetPeerDataRequestSessionID())).
|
||||
Any("access_status", notif.GetMessageAccessStatus())).
|
||||
Msg("Storing history sync")
|
||||
start := time.Now()
|
||||
successfullySavedTotal := 0
|
||||
|
|
@ -229,32 +201,30 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
|||
} else {
|
||||
totalMessageCount += len(conv.GetMessages())
|
||||
}
|
||||
if jid.Server == types.DefaultUserServer {
|
||||
lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, jid)
|
||||
if jid.Server == types.HiddenUserServer {
|
||||
pn, err := wa.GetStore().LIDs.GetPNForLID(ctx, jid)
|
||||
if err != nil {
|
||||
log.Err(err).Stringer("pn", jid).Msg("Failed to get LID for phone number in history sync")
|
||||
} else if lid.IsEmpty() {
|
||||
log.Warn().Stringer("pn", jid).Msg("No LID found for phone number in history sync")
|
||||
log.Err(err).Stringer("lid", jid).Msg("Failed to get PN for LID in history sync")
|
||||
} else if pn.IsEmpty() {
|
||||
log.Warn().Stringer("lid", jid).Msg("No PN found for LID in history sync")
|
||||
} else {
|
||||
log.Debug().
|
||||
Stringer("lid", lid).
|
||||
Stringer("pn", jid).
|
||||
Msg("Rerouting phone number DM to LID in history sync")
|
||||
jid = lid
|
||||
Stringer("lid", jid).
|
||||
Stringer("pn", pn).
|
||||
Msg("Rerouting LID DM to phone number in history sync")
|
||||
jid = pn
|
||||
}
|
||||
}
|
||||
log.UpdateContext(func(c zerolog.Context) zerolog.Context {
|
||||
return c.Stringer("chat_jid", jid)
|
||||
})
|
||||
|
||||
var firstItemTime, lastItemTime time.Time
|
||||
var minTime, maxTime time.Time
|
||||
var minTimeIndex, maxTimeIndex int
|
||||
|
||||
ignoredTypes := 0
|
||||
rawMessages := conv.GetMessages()
|
||||
messages := make([]*wadb.HistorySyncMessageTuple, 0, len(rawMessages))
|
||||
allowClamp := conv.GetCommentsCount() == 0
|
||||
var newerTS uint64
|
||||
for i, rawMsg := range rawMessages {
|
||||
messages := make([]*wadb.HistorySyncMessageTuple, 0, len(conv.GetMessages()))
|
||||
for i, rawMsg := range conv.GetMessages() {
|
||||
// Don't store messages that will just be skipped.
|
||||
msgEvt, err := wa.Client.ParseWebMessage(jid, rawMsg.GetMessage())
|
||||
if err != nil {
|
||||
|
|
@ -263,39 +233,22 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
|||
Str("msg_id", rawMsg.GetMessage().GetKey().GetID()).
|
||||
Uint64("msg_time_seconds", rawMsg.GetMessage().GetMessageTimestamp()).
|
||||
Msg("Dropping historical message due to parse error")
|
||||
log.Trace().
|
||||
Any("web_message_info", rawMsg.GetMessage()).
|
||||
Msg("Content of event that failed to parse")
|
||||
continue
|
||||
}
|
||||
if firstItemTime.IsZero() {
|
||||
firstItemTime = msgEvt.Info.Timestamp
|
||||
if minTime.IsZero() || msgEvt.Info.Timestamp.Before(minTime) {
|
||||
minTime = msgEvt.Info.Timestamp
|
||||
minTimeIndex = i
|
||||
}
|
||||
if maxTime.IsZero() || msgEvt.Info.Timestamp.After(maxTime) {
|
||||
maxTime = msgEvt.Info.Timestamp
|
||||
maxTimeIndex = i
|
||||
}
|
||||
lastItemTime = msgEvt.Info.Timestamp
|
||||
|
||||
msgType := getMessageType(msgEvt.Message)
|
||||
if msgType == "ignore" || strings.HasPrefix(msgType, "unknown_protocol_") {
|
||||
ignoredTypes++
|
||||
continue
|
||||
}
|
||||
// Comments (replies) in announcement groups are not ordered by timestamp, so don't clamp them.
|
||||
if rawMsg.GetMessage().GetCommentMetadata().GetCommentParentKey() != nil {
|
||||
allowClamp = false
|
||||
}
|
||||
// WhatsApp has bugs where some random messages will have timestamps decades in the future.
|
||||
// To ensure they don't mess up our ordering, require timestamps of older messages to be
|
||||
// before the previous (newer) message.
|
||||
if currentTS := rawMsg.GetMessage().GetMessageTimestamp(); newerTS > 0 && allowClamp && currentTS > newerTS {
|
||||
log.Warn().
|
||||
Time("current_ts", time.Unix(int64(currentTS), 0)).
|
||||
Time("prev_ts", time.Unix(int64(newerTS), 0)).
|
||||
Int("msg_index", i).
|
||||
Str("msg_id", rawMsg.GetMessage().GetKey().GetID()).
|
||||
Msg("Clamping message timestamp")
|
||||
rawMsg.Message.MessageTimestamp = ptr.Ptr(newerTS)
|
||||
msgEvt.Info.Timestamp = time.Unix(int64(newerTS), 0)
|
||||
}
|
||||
newerTS = rawMsg.GetMessage().GetMessageTimestamp()
|
||||
marshaled, err := proto.Marshal(rawMsg)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).
|
||||
|
|
@ -310,8 +263,10 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
|||
log.Debug().
|
||||
Int("wrapped_count", len(messages)).
|
||||
Int("ignored_msg_type_count", ignoredTypes).
|
||||
Time("first_item_time", firstItemTime).
|
||||
Time("last_item_time", lastItemTime).
|
||||
Time("lowest_time", minTime).
|
||||
Int("lowest_time_index", minTimeIndex).
|
||||
Time("highest_time", maxTime).
|
||||
Int("highest_time_index", maxTimeIndex).
|
||||
Dict("metadata", zerolog.Dict().
|
||||
Uint32("ephemeral_expiration", conv.GetEphemeralExpiration()).
|
||||
Int64("ephemeral_setting_timestamp", conv.GetEphemeralSettingTimestamp()).
|
||||
|
|
@ -320,17 +275,15 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
|||
Bool("archived", conv.GetArchived()).
|
||||
Uint32("pinned", conv.GetPinned()).
|
||||
Uint64("mute_end", conv.GetMuteEndTime()).
|
||||
Uint32("unread_count", conv.GetUnreadCount()).
|
||||
Bool("end_of_history", conv.GetEndOfHistoryTransfer()).
|
||||
Stringer("end_of_history_type", conv.GetEndOfHistoryTransferType()),
|
||||
Uint32("unread_count", conv.GetUnreadCount()),
|
||||
).
|
||||
Msg("Collected messages to save from history sync conversation")
|
||||
|
||||
if len(messages) > 0 {
|
||||
err = wa.Main.DB.Conversation.Put(ctx, wadb.NewConversation(wa.UserLogin.ID, jid, conv, firstItemTime))
|
||||
err = wa.Main.DB.Conversation.Put(ctx, wadb.NewConversation(wa.UserLogin.ID, jid, conv, maxTime))
|
||||
if err != nil {
|
||||
if stopOnError {
|
||||
return fmt.Errorf("failed to save conversation metadata for %s: %w", jid, err)
|
||||
return false, fmt.Errorf("failed to save conversation metadata for %s: %w", jid, err)
|
||||
}
|
||||
log.Err(err).Msg("Failed to save conversation metadata")
|
||||
continue
|
||||
|
|
@ -338,7 +291,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
|||
err = wa.Main.DB.Message.Put(ctx, wa.UserLogin.ID, jid, messages)
|
||||
if err != nil {
|
||||
if stopOnError {
|
||||
return fmt.Errorf("failed to save messages in %s: %w", jid, err)
|
||||
return false, fmt.Errorf("failed to save messages in %s: %w", jid, err)
|
||||
}
|
||||
log.Err(err).Msg("Failed to save messages")
|
||||
failedToSaveTotal += len(messages)
|
||||
|
|
@ -348,7 +301,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
|||
err = wa.Main.Bridge.DB.BackfillTask.MarkNotDone(ctx, wa.makeWAPortalKey(jid), wa.UserLogin.ID)
|
||||
if err != nil {
|
||||
if stopOnError {
|
||||
return fmt.Errorf("failed to mark backfill task as not done for %s: %w", jid, err)
|
||||
return false, fmt.Errorf("failed to mark backfill task as not done for %s: %w", jid, err)
|
||||
}
|
||||
log.Err(err).Msg("Failed to mark backfill task as not done")
|
||||
}
|
||||
|
|
@ -360,7 +313,9 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
|||
Int("total_message_count", totalMessageCount).
|
||||
Dur("duration", time.Since(start)).
|
||||
Msg("Finished storing history sync")
|
||||
return nil
|
||||
resetTimer := evt.GetSyncType() == waHistorySync.HistorySync_RECENT ||
|
||||
evt.GetSyncType() == waHistorySync.HistorySync_FULL
|
||||
return resetTimer, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) {
|
||||
|
|
@ -408,8 +363,8 @@ func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) {
|
|||
return
|
||||
}
|
||||
wrappedInfo, err := wa.getChatInfo(ctx, conv.ChatJID, conv, true)
|
||||
if errors.Is(err, whatsmeow.ErrNotInGroup) || errors.Is(err, whatsmeow.ErrGroupNotFound) {
|
||||
log.Debug().Err(err).Stringer("chat_jid", conv.ChatJID).
|
||||
if errors.Is(err, whatsmeow.ErrNotInGroup) {
|
||||
log.Debug().Stringer("chat_jid", conv.ChatJID).
|
||||
Msg("Skipping creating room because the user is not a participant")
|
||||
//err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, conv.ChatJID)
|
||||
//if err != nil {
|
||||
|
|
@ -482,39 +437,26 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if portalJID.Server == types.DefaultUserServer {
|
||||
zerolog.Ctx(ctx).Warn().Stringer("portal_jid", portalJID).Msg("FetchMessages called for phone number portal")
|
||||
}
|
||||
var markRead bool
|
||||
var startTime, endTime *time.Time
|
||||
var conv *wadb.Conversation
|
||||
if params.Forward || wa.Main.Config.HistorySync.BackwardsOnDemand {
|
||||
conv, err = wa.Main.DB.Conversation.Get(ctx, wa.UserLogin.ID, portalJID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get conversation from database: %w", err)
|
||||
}
|
||||
}
|
||||
if params.Forward {
|
||||
if params.AnchorMessage != nil {
|
||||
startTime = ptr.Ptr(params.AnchorMessage.Timestamp)
|
||||
}
|
||||
if conv != nil {
|
||||
conv, err := wa.Main.DB.Conversation.Get(ctx, wa.UserLogin.ID, portalJID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get conversation from database: %w", err)
|
||||
} else if conv != nil {
|
||||
markRead = !ptr.Val(conv.MarkedAsUnread) && ptr.Val(conv.UnreadCount) == 0
|
||||
}
|
||||
} else {
|
||||
if params.AnchorMessage != nil {
|
||||
endTime = ptr.Ptr(params.AnchorMessage.Timestamp)
|
||||
}
|
||||
if params.Cursor != "" {
|
||||
} else if params.Cursor != "" {
|
||||
endTimeUnix, err := strconv.ParseInt(string(params.Cursor), 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse cursor: %w", err)
|
||||
}
|
||||
cursorTime := time.Unix(endTimeUnix, 0)
|
||||
if endTime == nil || cursorTime.Before(*endTime) {
|
||||
endTime = &cursorTime
|
||||
}
|
||||
}
|
||||
endTime = ptr.Ptr(time.Unix(endTimeUnix, 0))
|
||||
} else if params.AnchorMessage != nil {
|
||||
endTime = ptr.Ptr(params.AnchorMessage.Timestamp)
|
||||
}
|
||||
var anchorID types.MessageID
|
||||
if params.AnchorMessage != nil {
|
||||
|
|
@ -523,31 +465,19 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
|
|||
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)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load messages from database: %w", err)
|
||||
} else if len(messages) == 0 || (len(messages) == 1 && anchorID != "" && messages[0].GetKey().GetID() == anchorID) {
|
||||
wa.deleteHistorySyncMessages(ctx, portalJID, 0, 0)
|
||||
if hasMore && !params.AllowSlowFetch {
|
||||
return &bridgev2.FetchMessagesResponse{
|
||||
MoreRequiresSlowFetch: true,
|
||||
HasMore: true,
|
||||
Forward: params.Forward,
|
||||
}, nil
|
||||
} else if hasMore {
|
||||
return wa.fetchMessagesFromPhone(ctx, portalJID, params)
|
||||
}
|
||||
return &bridgev2.FetchMessagesResponse{
|
||||
HasMore: false,
|
||||
Forward: params.Forward,
|
||||
}, nil
|
||||
}
|
||||
if len(messages) > params.Count {
|
||||
hasMore := false
|
||||
oldestTS := messages[len(messages)-1].GetMessageTimestamp()
|
||||
newestTS := messages[0].GetMessageTimestamp()
|
||||
if len(messages) > params.Count {
|
||||
hasMore = true
|
||||
// For safety, cut off messages with the oldest timestamp in the response.
|
||||
// Otherwise, if there are multiple messages with the same timestamp, the next fetch may miss some.
|
||||
|
|
@ -558,90 +488,19 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
|
|||
}
|
||||
}
|
||||
}
|
||||
resp, err := wa.convertHistorySyncMessages(ctx, params.Portal, portalJID, messages, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert messages: %w", err)
|
||||
}
|
||||
resp.HasMore = hasMore
|
||||
resp.Forward = params.Forward
|
||||
resp.MarkRead = markRead
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) deleteHistorySyncMessages(ctx context.Context, portalJID types.JID, newestTS, oldestTS uint64) {
|
||||
var err error
|
||||
var rows int64
|
||||
if (newestTS == 0 && oldestTS == 0) || !wa.Main.Bridge.Config.Backfill.Queue.AnyEnabled() {
|
||||
// If the backfill queue isn't enabled, delete all messages after backfilling a batch.
|
||||
rows, err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, portalJID)
|
||||
} else {
|
||||
// Otherwise just delete the messages that got backfilled
|
||||
rows, err = wa.Main.DB.Message.DeleteBetween(ctx, wa.UserLogin.ID, portalJID, newestTS, oldestTS)
|
||||
}
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).
|
||||
Stringer("portal_jid", portalJID).
|
||||
Uint64("newest_ts", newestTS).
|
||||
Uint64("oldest_ts", oldestTS).
|
||||
Msg("Failed to delete messages from database after backfill")
|
||||
} else {
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Stringer("portal_jid", portalJID).
|
||||
Uint64("newest_ts", newestTS).
|
||||
Uint64("oldest_ts", oldestTS).
|
||||
Int64("rows_affected", rows).
|
||||
Msg("Deleted history sync messages from database")
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) convertHistorySyncMessages(
|
||||
ctx context.Context,
|
||||
portal *bridgev2.Portal,
|
||||
portalJID types.JID,
|
||||
messages []*waWeb.WebMessageInfo,
|
||||
explodeOnError bool,
|
||||
) (*bridgev2.FetchMessagesResponse, error) {
|
||||
oldestTS := messages[len(messages)-1].GetMessageTimestamp()
|
||||
newestTS := messages[0].GetMessageTimestamp()
|
||||
convertedMessages := make([]*bridgev2.BackfillMessage, 0, len(messages))
|
||||
convertedMessages := make([]*bridgev2.BackfillMessage, len(messages))
|
||||
var mediaRequests []*wadb.MediaRequest
|
||||
dups := make(exmaps.Set[networkid.MessageID])
|
||||
for i, msg := range messages {
|
||||
evt, err := wa.Client.ParseWebMessage(portalJID, msg)
|
||||
if err != nil {
|
||||
if explodeOnError {
|
||||
// This should never happen because the info is already parsed once before being stored in the database
|
||||
return nil, fmt.Errorf("failed to parse info of message %s: %w", msg.GetKey().GetID(), err)
|
||||
}
|
||||
zerolog.Ctx(ctx).Warn().Err(err).
|
||||
Int("msg_index", i).
|
||||
Str("msg_id", msg.GetKey().GetID()).
|
||||
Uint64("msg_time_seconds", msg.GetMessageTimestamp()).
|
||||
Msg("Dropping historical message due to parse error")
|
||||
zerolog.Ctx(ctx).Trace().
|
||||
Any("web_message_info", msg.GetMessage()).
|
||||
Msg("Content of event that failed to parse")
|
||||
continue
|
||||
}
|
||||
if !explodeOnError {
|
||||
msgType := getMessageType(evt.Message)
|
||||
if msgType == "ignore" || strings.HasPrefix(msgType, "unknown_protocol_") {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, false) {
|
||||
return nil, fmt.Errorf("failed to ensure alt JIDs for message %s", evt.Info.ID)
|
||||
}
|
||||
var mediaReq *wadb.MediaRequest
|
||||
isViewOnce := evt.IsViewOnce || evt.IsViewOnceV2 || evt.IsViewOnceV2Extension
|
||||
converted, mediaReq := wa.convertHistorySyncMessage(
|
||||
ctx, portal, &evt.Info, evt.Message, evt.RawMessage, isViewOnce, msg.Reactions,
|
||||
convertedMessages[i], mediaReq = wa.convertHistorySyncMessage(
|
||||
ctx, params.Portal, &evt.Info, evt.Message, evt.RawMessage, isViewOnce, msg.Reactions,
|
||||
)
|
||||
// This is a hack to remove duplicates where the same message is inserted with both the LID and phone number sender
|
||||
// TODO prevent those being inserted in the first place instead of hacking around it here
|
||||
if !dups.Add(converted.ID) {
|
||||
continue
|
||||
}
|
||||
convertedMessages = append(convertedMessages, converted)
|
||||
if mediaReq != nil {
|
||||
mediaRequests = append(mediaRequests, mediaReq)
|
||||
}
|
||||
|
|
@ -650,10 +509,24 @@ func (wa *WhatsAppClient) convertHistorySyncMessages(
|
|||
return &bridgev2.FetchMessagesResponse{
|
||||
Messages: convertedMessages,
|
||||
Cursor: networkid.PaginationCursor(strconv.FormatUint(oldestTS, 10)),
|
||||
HasMore: hasMore,
|
||||
Forward: endTime == nil,
|
||||
MarkRead: markRead,
|
||||
// TODO set remaining or total count
|
||||
CompleteCallback: func() {
|
||||
// TODO this only deletes after backfilling. If there's no need for backfill after a relogin,
|
||||
// the messages will be stuck in the database
|
||||
wa.deleteHistorySyncMessages(ctx, portalJID, newestTS, oldestTS)
|
||||
var err error
|
||||
if !wa.Main.Bridge.Config.Backfill.Queue.Enabled && !wa.Main.Bridge.Config.Backfill.WillPaginateManually {
|
||||
// If the backfill queue isn't enabled, delete all messages after backfilling a batch.
|
||||
err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, portalJID)
|
||||
} else {
|
||||
// Otherwise just delete the messages that got backfilled
|
||||
err = wa.Main.DB.Message.DeleteBetween(ctx, wa.UserLogin.ID, portalJID, newestTS, oldestTS)
|
||||
}
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to delete messages from database after backfill")
|
||||
}
|
||||
if len(mediaRequests) > 0 {
|
||||
go func(ctx context.Context) {
|
||||
for _, req := range mediaRequests {
|
||||
|
|
@ -671,109 +544,6 @@ func (wa *WhatsAppClient) convertHistorySyncMessages(
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, portalJID types.JID, params bridgev2.FetchMessagesParams) (*bridgev2.FetchMessagesResponse, error) {
|
||||
if params.AnchorMessage == nil {
|
||||
return nil, fmt.Errorf("anchor message is required to fetch messages from phone")
|
||||
}
|
||||
parsed, err := waid.ParseMessageID(params.AnchorMessage.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse anchor message ID: %w", err)
|
||||
}
|
||||
|
||||
msgID := wa.Client.GenerateMessageID()
|
||||
reqData := wa.Client.BuildHistorySyncRequest(&types.MessageInfo{
|
||||
MessageSource: types.MessageSource{
|
||||
Chat: portalJID,
|
||||
Sender: parsed.Sender,
|
||||
IsFromMe: wa.IsOwnJID(parsed.Sender),
|
||||
IsGroup: parsed.Chat.Server == types.GroupServer,
|
||||
},
|
||||
ID: parsed.ID,
|
||||
Timestamp: params.AnchorMessage.Timestamp,
|
||||
}, 50)
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Str("request_msg_id", msgID).
|
||||
Stringer("portal_jid", portalJID).
|
||||
Any("anchor_msg_parsed", parsed).
|
||||
Any("request_data", reqData).
|
||||
Msg("Sending history sync request")
|
||||
_, err = wa.Client.SendMessage(ctx, wa.JID.ToNonAD(), reqData, whatsmeow.SendRequestExtra{
|
||||
ID: msgID,
|
||||
Peer: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to send history sync request: %w", err)
|
||||
}
|
||||
return &bridgev2.FetchMessagesResponse{
|
||||
HasMore: true,
|
||||
Pending: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleOnDemandHistorySync(ctx context.Context, blob *waHistorySync.HistorySync) {
|
||||
if len(blob.GetConversations()) > 1 {
|
||||
zerolog.Ctx(ctx).Warn().
|
||||
Int("conversation_count", len(blob.GetConversations())).
|
||||
Msg("Received on-demand history sync with multiple conversations")
|
||||
}
|
||||
for _, conv := range blob.GetConversations() {
|
||||
portalJID, err := types.ParseJID(conv.GetID())
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Str("jid", conv.GetID()).Msg("Failed to parse portal JID")
|
||||
continue
|
||||
}
|
||||
if portalJID.Server == types.DefaultUserServer {
|
||||
lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, portalJID)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Stringer("lid", portalJID).Msg("Failed to get LID for phone number in on-demand history sync")
|
||||
} else if lid.IsEmpty() {
|
||||
zerolog.Ctx(ctx).Warn().Stringer("lid", portalJID).Msg("No LID found for phone number in on-demand history sync")
|
||||
} else {
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Stringer("lid", lid).
|
||||
Stringer("pn", portalJID).
|
||||
Msg("Rerouting phone number DM to LID in on-demand history sync")
|
||||
portalJID = lid
|
||||
}
|
||||
}
|
||||
portal, err := wa.Main.Bridge.GetPortalByKey(ctx, wa.makeWAPortalKey(portalJID))
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Stringer("portal_jid", portalJID).Msg("Failed to get portal for on-demand history sync")
|
||||
continue
|
||||
}
|
||||
ctx := zerolog.Ctx(ctx).With().
|
||||
Str("portal_id", string(portal.ID)).
|
||||
Str("portal_receiver", string(portal.Receiver)).
|
||||
Stringer("portal_mxid", portal.MXID).
|
||||
Logger().WithContext(ctx)
|
||||
portal.HandleRemoteBackfill(ctx, wa.UserLogin, &simplevent.Backfill{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventBackfill,
|
||||
PortalKey: portal.PortalKey,
|
||||
},
|
||||
GetDataFunc: func(ctx context.Context, portal *bridgev2.Portal) (*bridgev2.FetchMessagesResponse, error) {
|
||||
if len(conv.GetMessages()) == 0 {
|
||||
return &bridgev2.FetchMessagesResponse{}, nil
|
||||
}
|
||||
messages := make([]*waWeb.WebMessageInfo, len(conv.GetMessages()))
|
||||
for i, rawMsg := range conv.GetMessages() {
|
||||
messages[i] = rawMsg.Message
|
||||
}
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Int("message_count", len(messages)).
|
||||
Stringer("end_of_history_type", conv.GetEndOfHistoryTransferType()).
|
||||
Msg("Converting messages to bridge from on-demand history sync")
|
||||
resp, err := wa.convertHistorySyncMessages(ctx, portal, portalJID, messages, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.HasMore = conv.GetEndOfHistoryTransferType() == waHistorySync.Conversation_COMPLETE_BUT_MORE_MESSAGES_REMAIN_ON_PRIMARY
|
||||
return resp, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) convertHistorySyncMessage(
|
||||
ctx context.Context, portal *bridgev2.Portal, info *types.MessageInfo, msg, rawMsg *waE2E.Message, isViewOnce bool, reactions []*waWeb.Reaction,
|
||||
) (*bridgev2.BackfillMessage, *wadb.MediaRequest) {
|
||||
|
|
@ -784,12 +554,11 @@ func (wa *WhatsAppClient) convertHistorySyncMessage(
|
|||
}
|
||||
// TODO use proper intent
|
||||
intent := wa.Main.Bridge.Bot
|
||||
msgID := waid.MakeMessageIDWithAltSender(info.Chat, info.Sender, info.SenderAlt, info.ID)
|
||||
wrapped := &bridgev2.BackfillMessage{
|
||||
ConvertedMessage: wa.Main.MsgConv.ToMatrix(ctx, portal, wa.Client, intent, msg, rawMsg, info, isViewOnce, true, nil),
|
||||
Sender: wa.makeEventSender(ctx, pickLID(info.Sender, info.SenderAlt)),
|
||||
ID: msgID,
|
||||
TxnID: networkid.TransactionID(msgID),
|
||||
Sender: wa.makeEventSender(ctx, info.Sender),
|
||||
ID: waid.MakeMessageID(info.Chat, info.Sender, info.ID),
|
||||
TxnID: networkid.TransactionID(waid.MakeMessageID(info.Chat, info.Sender, info.ID)),
|
||||
Timestamp: info.Timestamp,
|
||||
StreamOrder: info.Timestamp.Unix(),
|
||||
Reactions: make([]*bridgev2.BackfillReaction, 0, len(reactions)),
|
||||
|
|
@ -798,10 +567,10 @@ func (wa *WhatsAppClient) convertHistorySyncMessage(
|
|||
for _, reaction := range reactions {
|
||||
var sender types.JID
|
||||
if reaction.GetKey().GetFromMe() {
|
||||
sender = wa.GetLID()
|
||||
sender = wa.JID
|
||||
} else if reaction.GetKey().GetParticipant() != "" {
|
||||
sender, _ = types.ParseJID(*reaction.Key.Participant)
|
||||
} else if info.Chat.Server == types.DefaultUserServer || info.Chat.Server == types.HiddenUserServer || info.Chat.Server == types.BotServer {
|
||||
} else if info.Chat.Server == types.DefaultUserServer || info.Chat.Server == types.BotServer {
|
||||
sender = info.Chat
|
||||
}
|
||||
if sender.IsEmpty() {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ var WhatsAppGeneralCaps = &bridgev2.NetworkGeneralCapabilities{
|
|||
AggressiveUpdateInfo: true,
|
||||
ImplicitReadReceipts: true,
|
||||
Provisioning: bridgev2.ProvisioningCapabilities{
|
||||
ImagePackImport: true,
|
||||
ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{
|
||||
CreateDM: true,
|
||||
LookupPhone: true,
|
||||
|
|
@ -52,7 +51,7 @@ func (wa *WhatsAppConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilit
|
|||
}
|
||||
|
||||
func (wa *WhatsAppConnector) GetBridgeInfoVersion() (info, caps int) {
|
||||
return 1, 9
|
||||
return 1, 7
|
||||
}
|
||||
|
||||
const WAMaxFileSize = 2000 * 1024 * 1024
|
||||
|
|
@ -67,7 +66,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel {
|
|||
}
|
||||
|
||||
func capID() string {
|
||||
base := "fi.mau.whatsapp.capabilities.2026_07_22"
|
||||
base := "fi.mau.whatsapp.capabilities.2025_12_15"
|
||||
if ffmpeg.Supported() {
|
||||
return base + "+ffmpeg"
|
||||
}
|
||||
|
|
@ -126,10 +125,10 @@ var whatsappCaps = &event.RoomFeatures{
|
|||
event.CapMsgSticker: {
|
||||
MimeTypes: map[string]event.CapabilitySupportLevel{
|
||||
"image/webp": event.CapLevelFullySupported,
|
||||
// TODO see if sending lottie is possible
|
||||
//"video/lottie+json": event.CapLevelFullySupported,
|
||||
"image/png": event.CapLevelPartialSupport,
|
||||
"image/jpeg": event.CapLevelPartialSupport,
|
||||
// This will only be accepted if it was imported from WhatsApp
|
||||
"video/lottie+json": event.CapLevelPartialSupport,
|
||||
},
|
||||
Caption: event.CapLevelDropped,
|
||||
MaxSize: WAMaxFileSize,
|
||||
|
|
@ -177,11 +176,6 @@ var whatsappCaps = &event.RoomFeatures{
|
|||
MaxTextLength: MaxTextLength,
|
||||
LocationMessage: event.CapLevelFullySupported,
|
||||
Poll: event.CapLevelFullySupported,
|
||||
PollEnd: event.CapLevelUnsupported,
|
||||
PollHiddenVotes: event.CapLevelUnsupported,
|
||||
PollDuplicateOptions: event.CapLevelUnsupported,
|
||||
PollMaxOptions: 12,
|
||||
PollOptionMaxLength: 100,
|
||||
Reply: event.CapLevelFullySupported,
|
||||
Edit: event.CapLevelFullySupported,
|
||||
EditMaxAge: ptr.Ptr(jsontime.S(EditMaxAge)),
|
||||
|
|
|
|||
|
|
@ -99,18 +99,6 @@ func (wa *WhatsAppClient) applyChatSettings(ctx context.Context, chatID types.JI
|
|||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get chat settings")
|
||||
return
|
||||
}
|
||||
if !chat.Found {
|
||||
chatID, err = wa.GetStore().GetAltJID(ctx, chatID)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get alternate JID to get chat settings")
|
||||
return
|
||||
}
|
||||
chat, err = wa.GetStore().ChatSettings.GetChatSettings(ctx, chatID)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get chat settings with alternate JID")
|
||||
return
|
||||
}
|
||||
}
|
||||
info.UserLocal = &bridgev2.UserLocalPortalInfo{
|
||||
MutedUntil: ptr.Ptr(chat.MutedUntil),
|
||||
}
|
||||
|
|
@ -153,12 +141,7 @@ const PrivateChatTopic = "WhatsApp private chat"
|
|||
const BotChatTopic = "WhatsApp chat with a bot"
|
||||
|
||||
func (wa *WhatsAppClient) wrapDMInfo(ctx context.Context, jid types.JID) *bridgev2.ChatInfo {
|
||||
ownID := wa.JID
|
||||
if jid.Server == types.HiddenUserServer {
|
||||
ownID = wa.GetLID()
|
||||
}
|
||||
info := &bridgev2.ChatInfo{
|
||||
Type: ptr.Ptr(database.RoomTypeDM),
|
||||
Topic: ptr.Ptr(PrivateChatTopic),
|
||||
Members: &bridgev2.ChatMemberList{
|
||||
IsFull: true,
|
||||
|
|
@ -166,7 +149,7 @@ func (wa *WhatsAppClient) wrapDMInfo(ctx context.Context, jid types.JID) *bridge
|
|||
OtherUserID: waid.MakeUserID(jid),
|
||||
MemberMap: map[networkid.UserID]bridgev2.ChatMember{
|
||||
waid.MakeUserID(jid): {EventSender: wa.makeEventSender(ctx, jid)},
|
||||
waid.MakeUserID(ownID): {EventSender: wa.makeEventSender(ctx, ownID)},
|
||||
waid.MakeUserID(wa.JID): {EventSender: wa.makeEventSender(ctx, wa.JID)},
|
||||
},
|
||||
PowerLevels: &bridgev2.PowerLevelOverrides{
|
||||
Events: map[event.Type]int{
|
||||
|
|
@ -176,14 +159,13 @@ func (wa *WhatsAppClient) wrapDMInfo(ctx context.Context, jid types.JID) *bridge
|
|||
event.StateBeeperDisappearingTimer: 0,
|
||||
},
|
||||
},
|
||||
ExcludeChangesFromTimeline: true,
|
||||
},
|
||||
ExcludeChangesFromTimeline: true,
|
||||
Type: ptr.Ptr(database.RoomTypeDM),
|
||||
}
|
||||
if jid.Server == types.BotServer {
|
||||
info.Topic = ptr.Ptr(BotChatTopic)
|
||||
}
|
||||
if wa.IsOwnJID(jid) {
|
||||
if jid == wa.JID.ToNonAD() {
|
||||
// For chats with self, force-split the members so the user's own ghost is always in the room.
|
||||
info.Members.MemberMap = map[networkid.UserID]bridgev2.ChatMember{
|
||||
waid.MakeUserID(jid): {EventSender: bridgev2.EventSender{Sender: waid.MakeUserID(jid)}},
|
||||
|
|
@ -207,7 +189,7 @@ func (wa *WhatsAppClient) wrapStatusBroadcastInfo(ctx context.Context) *bridgev2
|
|||
Members: &bridgev2.ChatMemberList{
|
||||
IsFull: false,
|
||||
MemberMap: map[networkid.UserID]bridgev2.ChatMember{
|
||||
waid.MakeUserID(wa.GetLID()): {EventSender: wa.makeEventSender(ctx, wa.GetLID())},
|
||||
waid.MakeUserID(wa.JID): {EventSender: wa.makeEventSender(ctx, wa.JID)},
|
||||
},
|
||||
},
|
||||
Type: ptr.Ptr(database.RoomTypeDefault),
|
||||
|
|
@ -275,7 +257,7 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
|
|||
Name: ptr.Ptr(info.Name),
|
||||
Topic: ptr.Ptr(info.Topic),
|
||||
Members: &bridgev2.ChatMemberList{
|
||||
IsFull: !info.IsIncognito && !info.IsParent,
|
||||
IsFull: !info.IsIncognito,
|
||||
TotalMemberCount: len(info.Participants),
|
||||
MemberMap: make(map[networkid.UserID]bridgev2.ChatMember, len(info.Participants)),
|
||||
PowerLevels: &bridgev2.PowerLevelOverrides{
|
||||
|
|
@ -289,8 +271,9 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
|
|||
event.StateTopic: metaChangePL,
|
||||
event.EventReaction: defaultPL,
|
||||
event.EventRedaction: defaultPL,
|
||||
event.EventUnstablePollResponse: defaultPL,
|
||||
|
||||
event.StateBeeperDisappearingTimer: metaChangePL,
|
||||
// TODO always allow poll responses
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
@ -301,15 +284,11 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
|
|||
},
|
||||
ExtraUpdates: extraUpdater,
|
||||
}
|
||||
var hasSelf bool
|
||||
for _, pcp := range info.Participants {
|
||||
member := bridgev2.ChatMember{
|
||||
EventSender: wa.makeEventSender(ctx, pcp.JID),
|
||||
Membership: event.MembershipJoin,
|
||||
}
|
||||
if member.EventSender.IsFromMe {
|
||||
hasSelf = true
|
||||
}
|
||||
if pcp.IsSuperAdmin {
|
||||
member.PowerLevel = ptr.Ptr(superAdminPL)
|
||||
} else if pcp.IsAdmin {
|
||||
|
|
@ -320,20 +299,17 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
|
|||
member.MemberEventExtra = map[string]any{
|
||||
"com.beeper.exclude_from_timeline": true,
|
||||
}
|
||||
wrapped.Members.MemberMap.Set(member)
|
||||
wrapped.Members.MemberMap[waid.MakeUserID(pcp.JID)] = member
|
||||
if pcp.JID.Server == types.HiddenUserServer && !pcp.PhoneNumber.IsEmpty() {
|
||||
wrapped.Members.MemberMap.Add(bridgev2.ChatMember{
|
||||
wrapped.Members.MemberMap[waid.MakeUserID(pcp.PhoneNumber)] = bridgev2.ChatMember{
|
||||
EventSender: bridgev2.EventSender{Sender: waid.MakeUserID(pcp.PhoneNumber)},
|
||||
Membership: event.MembershipLeave,
|
||||
PrevMembership: event.MembershipJoin,
|
||||
MemberEventExtra: map[string]any{
|
||||
"com.beeper.exclude_from_timeline": true,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
if info.IsParent && !hasSelf && info.AddressingMode == types.AddressingModeLID {
|
||||
wrapped.Members.MemberMap.Add(bridgev2.ChatMember{EventSender: wa.makeEventSender(ctx, wa.GetLID())})
|
||||
}
|
||||
|
||||
if !info.LinkedParentJID.IsEmpty() {
|
||||
|
|
@ -474,7 +450,7 @@ func (wa *WhatsAppClient) makePortalAvatarFetcher(avatarID string, sender types.
|
|||
wrappedAvatar = &bridgev2.Avatar{
|
||||
ID: networkid.AvatarID(avatar.ID),
|
||||
Get: func(ctx context.Context) ([]byte, error) {
|
||||
return wa.Client.DownloadMediaWithOnlyPath(ctx, avatar.DirectPath)
|
||||
return wa.Client.DownloadMediaWithPath(ctx, avatar.DirectPath, nil, nil, nil, 0, "", "")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -513,7 +489,7 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne
|
|||
if info.ThreadMeta.Picture != nil {
|
||||
avatar.ID = networkid.AvatarID(info.ThreadMeta.Picture.ID)
|
||||
avatar.Get = func(ctx context.Context) ([]byte, error) {
|
||||
return wa.Client.DownloadMediaWithOnlyPath(ctx, info.ThreadMeta.Picture.DirectPath)
|
||||
return wa.Client.DownloadMediaWithPath(ctx, info.ThreadMeta.Picture.DirectPath, nil, nil, nil, 0, "", "")
|
||||
}
|
||||
} else if info.ThreadMeta.Preview.ID != "" {
|
||||
avatar.ID = networkid.AvatarID(info.ThreadMeta.Preview.ID)
|
||||
|
|
@ -524,7 +500,7 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne
|
|||
} else if meta.ThreadMeta.Picture == nil {
|
||||
return nil, fmt.Errorf("full res avatar info is missing")
|
||||
}
|
||||
return wa.Client.DownloadMediaWithOnlyPath(ctx, meta.ThreadMeta.Picture.DirectPath)
|
||||
return wa.Client.DownloadMediaWithPath(ctx, meta.ThreadMeta.Picture.DirectPath, nil, nil, nil, 0, "", "")
|
||||
}
|
||||
} else {
|
||||
avatar.ID = "remove"
|
||||
|
|
@ -540,8 +516,8 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne
|
|||
Members: &bridgev2.ChatMemberList{
|
||||
TotalMemberCount: info.ThreadMeta.SubscriberCount,
|
||||
MemberMap: map[networkid.UserID]bridgev2.ChatMember{
|
||||
waid.MakeUserID(wa.GetLID()): {
|
||||
EventSender: wa.makeEventSender(ctx, wa.GetLID()),
|
||||
waid.MakeUserID(wa.JID): {
|
||||
EventSender: wa.makeEventSender(ctx, wa.JID),
|
||||
PowerLevel: &ownPowerLevel,
|
||||
},
|
||||
},
|
||||
|
|
@ -555,7 +531,7 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne
|
|||
event.StateTopic: adminPL,
|
||||
event.EventReaction: defaultPL,
|
||||
event.EventRedaction: defaultPL,
|
||||
event.EventUnstablePollResponse: defaultPL,
|
||||
// TODO always allow poll responses
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -38,7 +38,6 @@ import (
|
|||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
"maunium.net/go/mautrix/bridgev2/status"
|
||||
"maunium.net/go/mautrix/event"
|
||||
|
||||
"go.mau.fi/mautrix-whatsapp/pkg/waid"
|
||||
)
|
||||
|
|
@ -54,9 +53,8 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2.
|
|||
directMediaRetries: make(map[networkid.MessageID]*directMediaRetry),
|
||||
mediaRetryLock: semaphore.NewWeighted(wa.Config.HistorySync.MediaRequests.MaxAsyncHandle),
|
||||
pushNamesSynced: exsync.NewEvent(),
|
||||
createDedup: exsync.NewSet[types.MessageID](),
|
||||
appStateFullSyncAttempted: make(map[appstate.WAPatchName]time.Time),
|
||||
|
||||
disableNewsletter: store.BaseClientPayload.GetUserAgent().GetPlatform() == waWa6.ClientPayload_UserAgent_MACOS,
|
||||
}
|
||||
login.Client = w
|
||||
|
||||
|
|
@ -71,7 +69,6 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2.
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
w.LID = w.Device.GetLID()
|
||||
|
||||
if w.Device != nil {
|
||||
log := w.UserLogin.Log.With().Str("component", "whatsmeow").Logger()
|
||||
|
|
@ -106,7 +103,6 @@ type WhatsAppClient struct {
|
|||
Client *whatsmeow.Client
|
||||
Device *store.Device
|
||||
JID types.JID
|
||||
LID types.JID
|
||||
MC mClient
|
||||
|
||||
historySyncWakeup chan struct{}
|
||||
|
|
@ -121,8 +117,7 @@ type WhatsAppClient struct {
|
|||
isNewLogin bool
|
||||
pushNamesSynced *exsync.Event
|
||||
lastPresence types.Presence
|
||||
|
||||
disableNewsletter bool
|
||||
createDedup *exsync.Set[types.MessageID]
|
||||
|
||||
appStateRecoveryLock sync.Mutex
|
||||
appStateFullSyncAttempted map[appstate.WAPatchName]time.Time
|
||||
|
|
@ -133,7 +128,6 @@ var (
|
|||
_ bridgev2.PushableNetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.BackgroundSyncingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.ChatViewingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.StickerImportingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
)
|
||||
|
||||
var pushCfg = &bridgev2.PushConfig{
|
||||
|
|
@ -189,19 +183,7 @@ func (wa *WhatsAppClient) RegisterPushNotifications(ctx context.Context, pushTyp
|
|||
}
|
||||
|
||||
func (wa *WhatsAppClient) IsThisUser(_ context.Context, userID networkid.UserID) bool {
|
||||
return userID == waid.MakeUserID(wa.JID) || userID == waid.MakeUserID(wa.GetLID())
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) IsOwnJID(jid types.JID) bool {
|
||||
return (jid.Server == types.DefaultUserServer && jid.User == wa.JID.User) ||
|
||||
(jid.Server == types.HiddenUserServer && jid.User == wa.GetLID().User)
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) GetLID() types.JID {
|
||||
if wa.LID.IsEmpty() && !wa.JID.IsEmpty() {
|
||||
wa.LID = wa.GetStore().GetLID()
|
||||
}
|
||||
return wa.LID
|
||||
return userID == waid.MakeUserID(wa.JID)
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) Connect(ctx context.Context) {
|
||||
|
|
@ -485,12 +467,3 @@ func (wa *WhatsAppClient) updatePresence(ctx context.Context, presence types.Pre
|
|||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) DownloadImagePack(ctx context.Context, url string) (*bridgev2.ImportedImagePack, error) {
|
||||
return wa.Main.MsgConv.DownloadImagePack(ctx, wa.UserLogin.ID, wa.Client, url)
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) ListImagePacks(ctx context.Context) ([]*event.ImagePackMetadata, error) {
|
||||
// TODO
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package connector
|
|||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
|
@ -71,8 +70,6 @@ type Config struct {
|
|||
RequestLocalTime int `yaml:"request_local_time"`
|
||||
MaxAsyncHandle int64 `yaml:"max_async_handle"`
|
||||
} `yaml:"media_requests"`
|
||||
|
||||
BackwardsOnDemand bool `yaml:"backwards_on_demand"`
|
||||
} `yaml:"history_sync"`
|
||||
|
||||
displaynameTemplate *template.Template `yaml:"-"`
|
||||
|
|
@ -91,15 +88,7 @@ func (c *Config) UnmarshalYAML(node *yaml.Node) error {
|
|||
func (c *Config) PostProcess() error {
|
||||
var err error
|
||||
c.displaynameTemplate, err = template.New("displayname").Parse(c.DisplaynameTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Try to execute template to make sure it's valid
|
||||
_, err = c.formatDisplayname(types.PSAJID, "", types.ContactInfo{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute displayname template: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func upgradeConfig(helper up.Helper) {
|
||||
|
|
@ -145,7 +134,6 @@ func upgradeConfig(helper up.Helper) {
|
|||
helper.Copy(up.Str, "history_sync", "media_requests", "request_method")
|
||||
helper.Copy(up.Int, "history_sync", "media_requests", "request_local_time")
|
||||
helper.Copy(up.Int, "history_sync", "media_requests", "max_async_handle")
|
||||
helper.Copy(up.Bool, "history_sync", "backwards_on_demand")
|
||||
}
|
||||
|
||||
type DisplaynameParams struct {
|
||||
|
|
@ -160,7 +148,7 @@ type DisplaynameParams struct {
|
|||
Short string
|
||||
}
|
||||
|
||||
func (c *Config) formatDisplayname(jid types.JID, phone string, contact types.ContactInfo) (string, error) {
|
||||
func (c *Config) FormatDisplayname(jid types.JID, phone string, contact types.ContactInfo) string {
|
||||
var nameBuf strings.Builder
|
||||
if phone == "" && jid.Server == types.DefaultUserServer {
|
||||
phone = "+" + jid.User
|
||||
|
|
@ -179,21 +167,13 @@ func (c *Config) formatDisplayname(jid types.JID, phone string, contact types.Co
|
|||
Name: contact.FullName,
|
||||
Short: contact.FirstName,
|
||||
})
|
||||
return nameBuf.String(), err
|
||||
}
|
||||
|
||||
func (c *Config) FormatDisplayname(jid types.JID, phone string, contact types.ContactInfo) string {
|
||||
name, err := c.formatDisplayname(jid, phone, contact)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return name
|
||||
return nameBuf.String()
|
||||
}
|
||||
|
||||
func redactPhone(phone string) string {
|
||||
if len(phone) <= 4 {
|
||||
return phone
|
||||
}
|
||||
// This doesn't keep 2+ digit country codes properly, but whatever
|
||||
return phone[:2] + strings.Repeat("∙", len(phone)-4) + phone[len(phone)-2:]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,24 +28,27 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/dbutil"
|
||||
"go.mau.fi/util/exsync"
|
||||
"go.mau.fi/util/random"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/proto/waCompanionReg"
|
||||
"go.mau.fi/whatsmeow/store"
|
||||
"go.mau.fi/whatsmeow/store/sqlstore"
|
||||
whatsmeowUpgrades "go.mau.fi/whatsmeow/store/sqlstore/upgrades"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
waLog "go.mau.fi/whatsmeow/util/log"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/commands"
|
||||
"maunium.net/go/mautrix/bridgev2/database"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
"go.mau.fi/mautrix-whatsapp/pkg/connector/wadb"
|
||||
"go.mau.fi/mautrix-whatsapp/pkg/msgconv"
|
||||
"go.mau.fi/mautrix-whatsapp/pkg/waid"
|
||||
)
|
||||
|
||||
type WhatsAppConnector struct {
|
||||
|
|
@ -61,8 +64,6 @@ type WhatsAppConnector struct {
|
|||
mediaEditCache MediaEditCache
|
||||
mediaEditCacheLock sync.RWMutex
|
||||
stopMediaEditCacheLoop atomic.Pointer[context.CancelFunc]
|
||||
|
||||
unmigratedDMs *exsync.Set[networkid.PortalKey]
|
||||
}
|
||||
|
||||
func init() {
|
||||
|
|
@ -113,7 +114,6 @@ func (wa *WhatsAppConnector) Init(bridge *bridgev2.Bridge) {
|
|||
cmdAccept, cmdSync, cmdInviteLink, cmdResolveLink, cmdJoin,
|
||||
)
|
||||
wa.mediaEditCache = make(MediaEditCache)
|
||||
wa.unmigratedDMs = exsync.NewSet[networkid.PortalKey]()
|
||||
|
||||
whatsmeowDBLog := bridge.Log.With().Str("db_section", "whatsmeow").Logger()
|
||||
wa.DeviceStore = sqlstore.NewWithWrappedDB(
|
||||
|
|
@ -157,7 +157,78 @@ func (wa *WhatsAppConnector) Start(ctx context.Context) error {
|
|||
return bridgev2.DBUpgradeError{Err: err, Section: "whatsapp"}
|
||||
}
|
||||
|
||||
return wa.migrateToLIDDMs(ctx)
|
||||
if !wa.Bridge.Background && wa.Bridge.DB.KV.Get(ctx, "whatsapp_lid_dms_deleted") == "false" {
|
||||
wa.deleteLIDDMsMigration(ctx)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppConnector) deleteLIDDMsMigration(ctx context.Context) {
|
||||
log := zerolog.Ctx(ctx).With().Str("action", "delete lid dms").Logger()
|
||||
portals, err := wa.Bridge.GetAllPortalsWithMXID(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to get portals for LID DM deletion")
|
||||
return
|
||||
}
|
||||
defer wa.Bridge.DB.KV.Set(ctx, "whatsapp_lid_dms_deleted", "true")
|
||||
if len(portals) == 0 {
|
||||
log.Debug().Msg("No portals found")
|
||||
return
|
||||
}
|
||||
portalsByKey := make(map[networkid.PortalKey]*bridgev2.Portal, len(portals))
|
||||
for _, p := range portals {
|
||||
if p.Receiver == "" || p.RoomType != database.RoomTypeDM {
|
||||
continue
|
||||
}
|
||||
portalsByKey[p.PortalKey] = p
|
||||
}
|
||||
_, err = wa.DB.Exec(ctx, "DELETE FROM whatsapp_history_sync_conversation WHERE chat_jid LIKE '%@lid'")
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to remove LID conversations from history sync")
|
||||
}
|
||||
for key, portal := range portalsByKey {
|
||||
parsedID, err := waid.ParsePortalID(key.ID)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("portal_id", string(key.ID)).Msg("Failed to parse portal ID")
|
||||
continue
|
||||
} else if parsedID.Server != types.HiddenUserServer {
|
||||
continue
|
||||
}
|
||||
var pnStr string
|
||||
err = wa.DB.QueryRow(ctx, "SELECT pn FROM whatsmeow_lid_map WHERE lid=$1", parsedID.User).Scan(&pnStr)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("portal_id", string(key.ID)).Msg("Failed to get PN for LID portal")
|
||||
continue
|
||||
}
|
||||
key.ID = waid.MakePortalID(types.JID{User: pnStr, Server: types.DefaultUserServer})
|
||||
_, pnPortalExists := portalsByKey[key]
|
||||
if !pnPortalExists {
|
||||
log.Warn().Str("portal_id", string(key.ID)).Msg("PN portal does not exist, not deleting LID DM")
|
||||
continue
|
||||
}
|
||||
err = portal.Delete(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).
|
||||
Object("portal_key", portal.PortalKey).
|
||||
Stringer("portal_mxid", portal.MXID).
|
||||
Msg("Failed to delete LID DM portal from database")
|
||||
continue
|
||||
}
|
||||
err = wa.Bridge.Bot.DeleteRoom(ctx, portal.MXID, false)
|
||||
if err != nil {
|
||||
log.Err(err).
|
||||
Object("portal_key", portal.PortalKey).
|
||||
Stringer("portal_mxid", portal.MXID).
|
||||
Msg("Failed to delete LID DM portal from Matrix")
|
||||
continue
|
||||
}
|
||||
log.Debug().
|
||||
Object("portal_key", portal.PortalKey).
|
||||
Stringer("portal_mxid", portal.MXID).
|
||||
Msg("Deleted LID DM portal")
|
||||
}
|
||||
log.Info().Msg("Finished deleting LID DM portals")
|
||||
}
|
||||
|
||||
func (wa *WhatsAppConnector) Stop() {
|
||||
|
|
|
|||
|
|
@ -17,7 +17,6 @@
|
|||
package connector
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
|
@ -52,7 +51,7 @@ func (wa *WhatsAppConnector) SetUseDirectMedia() {
|
|||
}
|
||||
|
||||
var ErrReloadNeeded = mautrix.RespError{
|
||||
ErrCode: "COM.BEEPER.MEDIA_RELOAD_NEEDED",
|
||||
ErrCode: "FI.MAU.WHATSAPP_RELOAD_NEEDED",
|
||||
Err: "Media is no longer available on WhatsApp servers and must be re-requested from your phone",
|
||||
StatusCode: http.StatusNotFound,
|
||||
}
|
||||
|
|
@ -68,8 +67,6 @@ func (wa *WhatsAppConnector) Download(ctx context.Context, mediaID networkid.Med
|
|||
return wa.downloadMessageDirectMedia(ctx, parsedID, params)
|
||||
} else if parsedID.Avatar != nil {
|
||||
return wa.downloadAvatarDirectMedia(ctx, parsedID, params)
|
||||
} else if parsedID.Sticker != nil {
|
||||
return wa.downloadStickerDirectMedia(ctx, parsedID, params)
|
||||
} else {
|
||||
return nil, fmt.Errorf("unexpected media ID parsing result")
|
||||
}
|
||||
|
|
@ -131,30 +128,15 @@ func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, pars
|
|||
}
|
||||
return &mediaproxy.GetMediaResponseFile{
|
||||
Callback: func(w *os.File) (*mediaproxy.FileMeta, error) {
|
||||
return &mediaproxy.FileMeta{}, waClient.Client.DownloadMediaWithOnlyPathToFile(ctx, cachedInfo.DirectPath, w)
|
||||
return &mediaproxy.FileMeta{}, waClient.Client.DownloadMediaWithPathToFile(
|
||||
ctx, cachedInfo.DirectPath, nil, nil, nil, 0, "", "", w,
|
||||
)
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppConnector) downloadStickerDirectMedia(ctx context.Context, parsedID *waid.ParsedMediaID, params map[string]string) (mediaproxy.GetMediaResponse, error) {
|
||||
ul := wa.Bridge.GetCachedUserLoginByID(parsedID.UserLogin)
|
||||
if ul == nil {
|
||||
return nil, fmt.Errorf("%w: user login %s not found", bridgev2.ErrNotLoggedIn, parsedID.UserLogin)
|
||||
}
|
||||
waClient := ul.Client.(*WhatsAppClient)
|
||||
if waClient.Client == nil {
|
||||
return nil, fmt.Errorf("no WhatsApp client found on login %s", parsedID.UserLogin)
|
||||
}
|
||||
sticker, err := wa.MsgConv.GetCachedSticker(ctx, waClient.Client, parsedID.Sticker.PackID, parsedID.Sticker.FileHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if sticker == nil {
|
||||
return nil, mautrix.MNotFound.WithMessage("Sticker not found in pack")
|
||||
}
|
||||
return wa.makeDirectMediaResponse(ctx, waClient, sticker, sticker.MimeType, "", nil, params)
|
||||
}
|
||||
|
||||
func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, parsedID *waid.ParsedMediaID, params map[string]string) (mediaproxy.GetMediaResponse, error) {
|
||||
log := zerolog.Ctx(ctx)
|
||||
msg, err := wa.Bridge.DB.Message.GetFirstPartByID(ctx, parsedID.UserLogin, parsedID.Message.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get message: %w", err)
|
||||
|
|
@ -170,9 +152,6 @@ func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, par
|
|||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal media keys: %w", err)
|
||||
}
|
||||
if version := parsedID.Message.Version; len(version) > 0 && !bytes.Equal(version, keys.EncSHA256) && !bytes.Equal(version, keys.SHA256) {
|
||||
return nil, mautrix.MNotFound.WithMessage("Version mismatch, media was likely replaced")
|
||||
}
|
||||
var ul *bridgev2.UserLogin
|
||||
if parsedID.UserLogin != "" {
|
||||
ul = wa.Bridge.GetCachedUserLoginByID(parsedID.UserLogin)
|
||||
|
|
@ -195,29 +174,16 @@ func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, par
|
|||
if waClient.Client == nil {
|
||||
return nil, fmt.Errorf("no WhatsApp client found on login")
|
||||
}
|
||||
return wa.makeDirectMediaResponse(ctx, waClient, keys, keys.MimeType, msg.ID, keys, params)
|
||||
}
|
||||
|
||||
func (wa *WhatsAppConnector) makeDirectMediaResponse(
|
||||
ctx context.Context,
|
||||
waClient *WhatsAppClient,
|
||||
dm whatsmeow.DownloadableMessage,
|
||||
mimeType string,
|
||||
msgID networkid.MessageID,
|
||||
keys *msgconv.FailedMediaKeys,
|
||||
params map[string]string,
|
||||
) (mediaproxy.GetMediaResponse, error) {
|
||||
return &mediaproxy.GetMediaResponseFile{
|
||||
Callback: func(f *os.File) (*mediaproxy.FileMeta, error) {
|
||||
log := zerolog.Ctx(ctx)
|
||||
err := waClient.Client.DownloadToFile(ctx, dm, f)
|
||||
if keys != nil && (errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith403) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith410) || errors.Is(err, whatsmeow.ErrNoURLPresent)) {
|
||||
val := params["com.beeper.interactive_download_request"]
|
||||
err := waClient.Client.DownloadToFile(ctx, keys, f)
|
||||
if errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith403) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith410) || errors.Is(err, whatsmeow.ErrNoURLPresent) {
|
||||
val := params["fi.mau.whatsapp.reload_media"]
|
||||
if val == "false" || (!wa.Config.DirectMediaAutoRequest && val != "true") {
|
||||
return nil, ErrReloadNeeded
|
||||
}
|
||||
log.Trace().Msg("Media not found for direct download, requesting and waiting")
|
||||
err = waClient.requestAndWaitDirectMedia(ctx, msgID, keys)
|
||||
err = waClient.requestAndWaitDirectMedia(ctx, msg.ID, keys)
|
||||
if err != nil {
|
||||
log.Trace().Err(err).Msg("Failed to wait for media for direct download")
|
||||
return nil, err
|
||||
|
|
@ -225,29 +191,30 @@ func (wa *WhatsAppConnector) makeDirectMediaResponse(
|
|||
log.Trace().Msg("Retrying download after successful retry")
|
||||
err = waClient.Client.DownloadToFile(ctx, keys, f)
|
||||
}
|
||||
if errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
|
||||
if errors.Is(err, whatsmeow.ErrFileLengthMismatch) || errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Mismatching media checksums in message. Ignoring because WhatsApp seems to ignore them too")
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if mimeType == "application/was" {
|
||||
mime := keys.MimeType
|
||||
if mime == "application/was" {
|
||||
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
||||
return nil, fmt.Errorf("failed to seek to start of sticker zip: %w", err)
|
||||
} else if zipData, err := io.ReadAll(f); err != nil {
|
||||
return nil, fmt.Errorf("failed to read sticker zip: %w", err)
|
||||
} else if data, _, err := msgconv.ExtractAnimatedSticker(zipData); err != nil {
|
||||
} else if data, err := msgconv.ExtractAnimatedSticker(zipData); err != nil {
|
||||
return nil, fmt.Errorf("failed to extract animated sticker: %w %x", err, zipData)
|
||||
} else if _, err := f.WriteAt(data, 0); err != nil {
|
||||
return nil, fmt.Errorf("failed to write animated sticker to file: %w", err)
|
||||
} else if err := f.Truncate(int64(len(data))); err != nil {
|
||||
return nil, fmt.Errorf("failed to truncate animated sticker file: %w", err)
|
||||
}
|
||||
mimeType = "video/lottie+json"
|
||||
mime = "video/lottie+json"
|
||||
}
|
||||
|
||||
return &mediaproxy.FileMeta{
|
||||
ContentType: mimeType,
|
||||
ContentType: mime,
|
||||
}, nil
|
||||
},
|
||||
}, nil
|
||||
|
|
|
|||
|
|
@ -39,6 +39,18 @@ import (
|
|||
"go.mau.fi/mautrix-whatsapp/pkg/waid"
|
||||
)
|
||||
|
||||
func (wa *WhatsAppClient) getPortalKeyByMessageSource(ms types.MessageSource) networkid.PortalKey {
|
||||
jid := ms.Chat
|
||||
if ms.IsIncomingBroadcast() {
|
||||
if ms.IsFromMe {
|
||||
jid = ms.BroadcastListOwner.ToNonAD()
|
||||
} else {
|
||||
jid = ms.Sender.ToNonAD()
|
||||
}
|
||||
}
|
||||
return wa.makeWAPortalKey(jid)
|
||||
}
|
||||
|
||||
type MessageInfoWrapper struct {
|
||||
Info types.MessageInfo
|
||||
wa *WhatsAppClient
|
||||
|
|
@ -49,26 +61,7 @@ func (evt *MessageInfoWrapper) ShouldCreatePortal() bool {
|
|||
}
|
||||
|
||||
func (evt *MessageInfoWrapper) GetPortalKey() networkid.PortalKey {
|
||||
ms := evt.Info.MessageSource
|
||||
jid := ms.Chat
|
||||
if ms.IsIncomingBroadcast() {
|
||||
if ms.IsFromMe {
|
||||
// TODO can this still be a phone number?
|
||||
jid = ms.BroadcastListOwner.ToNonAD()
|
||||
} else {
|
||||
jid = ms.Sender.ToNonAD()
|
||||
if jid.Server == types.DefaultUserServer && !ms.SenderAlt.IsEmpty() {
|
||||
jid = ms.SenderAlt.ToNonAD()
|
||||
}
|
||||
}
|
||||
} else if jid.Server == types.DefaultUserServer {
|
||||
if !ms.IsFromMe && ms.Chat.ToNonAD() == ms.Sender.ToNonAD() && !ms.SenderAlt.IsEmpty() {
|
||||
jid = ms.SenderAlt.ToNonAD()
|
||||
} else if !ms.RecipientAlt.IsEmpty() {
|
||||
jid = ms.RecipientAlt.ToNonAD()
|
||||
}
|
||||
}
|
||||
return evt.wa.makeWAPortalKey(jid)
|
||||
return evt.wa.getPortalKeyByMessageSource(evt.Info.MessageSource)
|
||||
}
|
||||
|
||||
func (evt *MessageInfoWrapper) AddLogContext(c zerolog.Context) zerolog.Context {
|
||||
|
|
@ -79,19 +72,12 @@ func (evt *MessageInfoWrapper) GetTimestamp() time.Time {
|
|||
return evt.Info.Timestamp
|
||||
}
|
||||
|
||||
func pickLID(main, alt types.JID) types.JID {
|
||||
if main.Server == types.DefaultUserServer && alt.Server == types.HiddenUserServer {
|
||||
return alt
|
||||
}
|
||||
return main
|
||||
}
|
||||
|
||||
func (evt *MessageInfoWrapper) GetSender() bridgev2.EventSender {
|
||||
return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, pickLID(evt.Info.Sender, evt.Info.SenderAlt))
|
||||
return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, evt.Info.Sender)
|
||||
}
|
||||
|
||||
func (evt *MessageInfoWrapper) GetID() networkid.MessageID {
|
||||
return waid.MakeMessageIDWithAltSender(evt.Info.Chat, evt.Info.Sender, evt.Info.SenderAlt, evt.Info.ID)
|
||||
return waid.MakeMessageID(evt.Info.Chat, evt.Info.Sender, evt.Info.ID)
|
||||
}
|
||||
|
||||
func (evt *MessageInfoWrapper) GetTransactionID() networkid.TransactionID {
|
||||
|
|
@ -105,7 +91,6 @@ type WAMessageEvent struct {
|
|||
|
||||
parsedMessageType string
|
||||
isUndecryptableUpsertSubEvent bool
|
||||
dontRenderEdited bool
|
||||
postHandle func()
|
||||
}
|
||||
|
||||
|
|
@ -148,6 +133,14 @@ func (evt *WAMessageEvent) PreHandle(ctx context.Context, portal *bridgev2.Porta
|
|||
return
|
||||
}
|
||||
meta := portal.Metadata.(*waid.PortalMetadata)
|
||||
if meta.AddressingMode == types.AddressingModeLID && evt.Info.Sender.Server == types.DefaultUserServer {
|
||||
evt.Info.Sender, evt.Info.SenderAlt = evt.Info.SenderAlt, evt.Info.Sender
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Stringer("lid", evt.Info.Sender).
|
||||
Stringer("pn", evt.Info.SenderAlt).
|
||||
Str("message_id", evt.Info.ID).
|
||||
Msg("Forced phone number sender to LID in group message")
|
||||
}
|
||||
if meta.AddressingMode == types.AddressingModeLID || meta.LIDMigrationAttempted {
|
||||
return
|
||||
}
|
||||
|
|
@ -166,6 +159,13 @@ func (evt *WAMessageEvent) PreHandle(ctx context.Context, portal *bridgev2.Porta
|
|||
log.Info().Msg("Resyncing group members as it appears to have switched to LID addressing mode")
|
||||
portal.UpdateInfo(ctx, evt.wa.wrapGroupInfo(ctx, info), evt.wa.UserLogin, nil, time.Time{})
|
||||
log.Debug().Msg("Finished resyncing after LID change")
|
||||
if evt.Info.Sender.Server == types.DefaultUserServer && evt.Info.SenderAlt.Server == types.HiddenUserServer {
|
||||
evt.Info.Sender, evt.Info.SenderAlt = evt.Info.SenderAlt, evt.Info.Sender
|
||||
log.Debug().
|
||||
Stringer("new_sender", evt.Info.Sender).
|
||||
Stringer("new_sender_alt", evt.Info.SenderAlt).
|
||||
Msg("Overriding sender to LID after resyncing group members")
|
||||
}
|
||||
}
|
||||
|
||||
func (evt *WAMessageEvent) PostHandle(ctx context.Context, portal *bridgev2.Portal) {
|
||||
|
|
@ -181,15 +181,12 @@ func (evt *WAMessageEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Por
|
|||
}
|
||||
var editedMsg *waE2E.Message
|
||||
var previouslyConvertedPart *bridgev2.ConvertedMessagePart
|
||||
targetMessage := evt.GetTargetMessage()
|
||||
cacheMessage := targetMessage
|
||||
if evt.isUndecryptableUpsertSubEvent {
|
||||
// TODO db metadata needs to be updated in this case to remove the error
|
||||
editedMsg = evt.Message
|
||||
cacheMessage = evt.GetID()
|
||||
} else {
|
||||
editedMsg = evt.Message.GetProtocolMessage().GetEditedMessage()
|
||||
previouslyConvertedPart = evt.wa.Main.GetMediaEditCache(portal, targetMessage)
|
||||
previouslyConvertedPart = evt.wa.Main.GetMediaEditCache(portal, evt.GetTargetMessage())
|
||||
meta := existing[0].Metadata.(*waid.MessageMetadata)
|
||||
if slices.Contains(meta.Edits, evt.Info.ID) {
|
||||
return nil, fmt.Errorf("%w: edit already handled", bridgev2.ErrIgnoringRemoteEvent)
|
||||
|
|
@ -205,11 +202,9 @@ func (evt *WAMessageEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Por
|
|||
evt.postHandle = func() {
|
||||
evt.wa.processFailedMedia(ctx, portal.PortalKey, evt.GetID(), cm, false)
|
||||
}
|
||||
} else if len(cm.Parts) > 0 && cacheMessage != "" {
|
||||
evt.wa.Main.AddMediaEditCache(portal, cacheMessage, cm.Parts[0])
|
||||
}
|
||||
editPart := cm.Parts[0].ToEditPart(existing[0])
|
||||
if evt.isUndecryptableUpsertSubEvent || evt.dontRenderEdited {
|
||||
if evt.isUndecryptableUpsertSubEvent {
|
||||
if editPart.TopLevelExtra == nil {
|
||||
editPart.TopLevelExtra = make(map[string]any)
|
||||
}
|
||||
|
|
@ -391,8 +386,6 @@ func (evt *WAUndecryptableMessage) GetStreamOrder() int64 {
|
|||
type WAMediaRetry struct {
|
||||
*events.MediaRetry
|
||||
wa *WhatsAppClient
|
||||
senderLID types.JID
|
||||
chatLID types.JID
|
||||
}
|
||||
|
||||
func (evt *WAMediaRetry) GetType() bridgev2.RemoteEventType {
|
||||
|
|
@ -400,7 +393,7 @@ func (evt *WAMediaRetry) GetType() bridgev2.RemoteEventType {
|
|||
}
|
||||
|
||||
func (evt *WAMediaRetry) GetPortalKey() networkid.PortalKey {
|
||||
return evt.wa.makeWAPortalKey(pickLID(evt.ChatID, evt.chatLID))
|
||||
return evt.wa.makeWAPortalKey(evt.ChatID)
|
||||
}
|
||||
|
||||
func (evt *WAMediaRetry) AddLogContext(c zerolog.Context) zerolog.Context {
|
||||
|
|
@ -414,23 +407,16 @@ func (evt *WAMediaRetry) AddLogContext(c zerolog.Context) zerolog.Context {
|
|||
|
||||
func (evt *WAMediaRetry) getRealSender() types.JID {
|
||||
sender := evt.SenderID
|
||||
if sender.IsEmpty() {
|
||||
if evt.FromMe {
|
||||
if evt.ChatID.Server == types.HiddenUserServer {
|
||||
sender = evt.wa.GetLID().ToNonAD()
|
||||
} else {
|
||||
sender = evt.wa.JID.ToNonAD()
|
||||
}
|
||||
} else if evt.ChatID.Server == types.DefaultUserServer || evt.ChatID.Server == types.HiddenUserServer || evt.ChatID.Server == types.BotServer {
|
||||
} else if sender.IsEmpty() && (evt.ChatID.Server == types.DefaultUserServer || evt.ChatID.Server == types.BotServer) {
|
||||
sender = evt.ChatID.ToNonAD()
|
||||
}
|
||||
}
|
||||
return sender
|
||||
}
|
||||
|
||||
func (evt *WAMediaRetry) GetSender() bridgev2.EventSender {
|
||||
realSender := pickLID(evt.getRealSender(), evt.senderLID)
|
||||
return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, realSender)
|
||||
return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, evt.getRealSender())
|
||||
}
|
||||
|
||||
func (evt *WAMediaRetry) GetTargetMessage() networkid.MessageID {
|
||||
|
|
|
|||
|
|
@ -121,6 +121,3 @@ history_sync:
|
|||
request_local_time: 120
|
||||
# Maximum number of media request responses to handle in parallel per user.
|
||||
max_async_handle: 2
|
||||
# Use on-demand history sync requests for fetching older messages?
|
||||
# This only applies when using the backfill queue, never for forward backfills.
|
||||
backwards_on_demand: false
|
||||
|
|
|
|||
|
|
@ -48,12 +48,11 @@ var (
|
|||
_ bridgev2.DeleteChatHandlingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
)
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixPollStart(ctx context.Context, msg *bridgev2.MatrixPollStart) (result *bridgev2.MatrixMessageResponse, retErr error) {
|
||||
func (wa *WhatsAppClient) HandleMatrixPollStart(ctx context.Context, msg *bridgev2.MatrixPollStart) (*bridgev2.MatrixMessageResponse, error) {
|
||||
waMsg, optionMap, err := wa.Main.MsgConv.PollStartToWhatsApp(ctx, msg.Content, msg.ReplyTo, msg.Portal)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert poll vote: %w", err)
|
||||
}
|
||||
defer wa.mcTrack(msg, time.Now(), &retErr)
|
||||
resp, err := wa.handleConvertedMatrixMessage(ctx, &msg.MatrixMessage, waMsg, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -68,21 +67,19 @@ func (wa *WhatsAppClient) HandleMatrixPollStart(ctx context.Context, msg *bridge
|
|||
return resp, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixPollVote(ctx context.Context, msg *bridgev2.MatrixPollVote) (result *bridgev2.MatrixMessageResponse, retErr error) {
|
||||
func (wa *WhatsAppClient) HandleMatrixPollVote(ctx context.Context, msg *bridgev2.MatrixPollVote) (*bridgev2.MatrixMessageResponse, error) {
|
||||
waMsg, err := wa.Main.MsgConv.PollVoteToWhatsApp(ctx, wa.Client, msg.Content, msg.VoteTo)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert poll vote: %w", err)
|
||||
}
|
||||
defer wa.mcTrack(msg, time.Now(), &retErr)
|
||||
return wa.handleConvertedMatrixMessage(ctx, &msg.MatrixMessage, waMsg, nil)
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (result *bridgev2.MatrixMessageResponse, retErr error) {
|
||||
func (wa *WhatsAppClient) HandleMatrixMessage(ctx context.Context, msg *bridgev2.MatrixMessage) (*bridgev2.MatrixMessageResponse, error) {
|
||||
waMsg, req, err := wa.Main.MsgConv.ToWhatsApp(ctx, wa.Client, msg.Event, msg.Content, msg.ReplyTo, msg.ThreadRoot, msg.Portal)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert message: %w", err)
|
||||
}
|
||||
defer wa.mcTrack(msg, time.Now(), &retErr)
|
||||
return wa.handleConvertedMatrixMessage(ctx, msg, waMsg, req)
|
||||
}
|
||||
|
||||
|
|
@ -103,23 +100,19 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if chatJID.Server == types.DefaultUserServer {
|
||||
zerolog.Ctx(ctx).Warn().Stringer("portal_jid", chatJID).Msg("Matrix message received in phone number portal")
|
||||
}
|
||||
if chatJID == types.StatusBroadcastJID && wa.Main.Config.DisableStatusBroadcastSend {
|
||||
return nil, ErrBroadcastSendDisabled
|
||||
}
|
||||
wrappedMsgID := waid.MakeMessageID(chatJID, wa.JID, req.ID)
|
||||
wrappedMsgID2 := waid.MakeMessageID(chatJID, wa.GetLID(), req.ID)
|
||||
wrappedMsgID2 := waid.MakeMessageID(chatJID, wa.GetStore().GetLID(), req.ID)
|
||||
msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID))
|
||||
msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID2))
|
||||
zerolog.Ctx(ctx).Trace().Any("payload", waMsg).Msg("Outgoing message payload")
|
||||
resp, err := wa.Client.SendMessage(ctx, chatJID, waMsg, *req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var pickedMessageID networkid.MessageID
|
||||
if resp.Sender == wa.GetLID() {
|
||||
if resp.Sender == wa.GetStore().GetLID() && chatJID.Server != types.DefaultUserServer {
|
||||
pickedMessageID = wrappedMsgID2
|
||||
msg.RemovePending(networkid.TransactionID(wrappedMsgID))
|
||||
} else {
|
||||
|
|
@ -140,17 +133,18 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) PreHandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (bridgev2.MatrixReactionPreResponse, error) {
|
||||
func (wa *WhatsAppClient) PreHandleMatrixReaction(_ context.Context, msg *bridgev2.MatrixReaction) (bridgev2.MatrixReactionPreResponse, error) {
|
||||
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return bridgev2.MatrixReactionPreResponse{}, fmt.Errorf("failed to parse portal ID: %w", err)
|
||||
} else if portalJID == types.StatusBroadcastJID {
|
||||
return bridgev2.MatrixReactionPreResponse{}, ErrBroadcastReactionUnsupported
|
||||
}
|
||||
sender := wa.GetLID()
|
||||
if portalJID.Server == types.DefaultUserServer {
|
||||
zerolog.Ctx(ctx).Warn().Stringer("portal_jid", portalJID).Msg("Matrix reaction received in phone number portal")
|
||||
sender = wa.JID
|
||||
sender := wa.JID
|
||||
if portalJID.Server == types.HiddenUserServer ||
|
||||
msg.Portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup ||
|
||||
msg.Portal.Metadata.(*waid.PortalMetadata).AddressingMode == types.AddressingModeLID {
|
||||
sender = wa.GetStore().GetLID()
|
||||
}
|
||||
return bridgev2.MatrixReactionPreResponse{
|
||||
SenderID: waid.MakeUserID(sender),
|
||||
|
|
@ -159,7 +153,7 @@ func (wa *WhatsAppClient) PreHandleMatrixReaction(ctx context.Context, msg *brid
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (result *database.Reaction, retErr error) {
|
||||
func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (*database.Reaction, error) {
|
||||
messageID, err := waid.ParseMessageID(msg.TargetMessage.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse target message ID: %w", err)
|
||||
|
|
@ -176,10 +170,9 @@ func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev
|
|||
SenderTimestampMS: proto.Int64(msg.Event.Timestamp),
|
||||
},
|
||||
}
|
||||
defer wa.mcTrack(msg, time.Now(), &retErr)
|
||||
var req whatsmeow.SendRequestExtra
|
||||
if msg.Portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup {
|
||||
reactionMsg.EncReactionMessage, err = wa.Client.EncryptReaction(ctx, msgconv.MessageIDToInfo(ctx, wa.Client, messageID), reactionMsg.ReactionMessage)
|
||||
reactionMsg.EncReactionMessage, err = wa.Client.EncryptReaction(ctx, msgconv.MessageIDToInfo(wa.Client, messageID), reactionMsg.ReactionMessage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encrypt reaction: %w", err)
|
||||
}
|
||||
|
|
@ -198,7 +191,7 @@ func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev
|
|||
}, err
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) (retErr error) {
|
||||
func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) error {
|
||||
messageID, err := waid.ParseMessageID(msg.TargetReaction.MessageID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse target message ID: %w", err)
|
||||
|
|
@ -222,13 +215,12 @@ func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *b
|
|||
extra.ID = types.MessageID(msg.InputTransactionID)
|
||||
}
|
||||
|
||||
defer wa.mcTrack(msg, time.Now(), &retErr)
|
||||
resp, err := wa.Client.SendMessage(ctx, portalJID, reactionMsg, extra)
|
||||
zerolog.Ctx(ctx).Trace().Any("response", resp).Msg("WhatsApp reaction response")
|
||||
return err
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.MatrixEdit) (retErr error) {
|
||||
func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.MatrixEdit) error {
|
||||
log := zerolog.Ctx(ctx)
|
||||
|
||||
var editID types.MessageID
|
||||
|
|
@ -252,8 +244,6 @@ func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.M
|
|||
if err != nil {
|
||||
return fmt.Errorf("failed to convert message: %w", err)
|
||||
}
|
||||
defer wa.mcTrack(edit, time.Now(), &retErr)
|
||||
|
||||
convertedEdit := wa.Client.BuildEdit(messageID.Chat, messageID.ID, waMsg)
|
||||
if edit.OrigSender == nil {
|
||||
convertedEdit.EditedMessage.Message.ProtocolMessage.TimestampMS = proto.Int64(edit.Event.Timestamp)
|
||||
|
|
@ -268,7 +258,7 @@ func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.M
|
|||
return err
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) (retErr error) {
|
||||
func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *bridgev2.MatrixMessageRemove) error {
|
||||
log := zerolog.Ctx(ctx)
|
||||
messageID, err := waid.ParseMessageID(msg.TargetMessage.ID)
|
||||
if err != nil {
|
||||
|
|
@ -280,7 +270,6 @@ func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *br
|
|||
return fmt.Errorf("failed to parse portal ID: %w", err)
|
||||
}
|
||||
|
||||
defer wa.mcTrack(msg, time.Now(), &retErr)
|
||||
revokeMessage := wa.Client.BuildRevoke(messageID.Chat, messageID.Sender, messageID.ID)
|
||||
|
||||
extra := whatsmeow.SendRequestExtra{}
|
||||
|
|
@ -293,7 +282,7 @@ func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *br
|
|||
return err
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bridgev2.MatrixReadReceipt) (retErr error) {
|
||||
func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *bridgev2.MatrixReadReceipt) error {
|
||||
if !receipt.ReadUpTo.After(receipt.LastRead) {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -322,7 +311,7 @@ func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *
|
|||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if wa.IsOwnJID(parsed.Sender) {
|
||||
if parsed.Sender.User == wa.GetStore().GetLID().User || parsed.Sender.User == wa.JID.User {
|
||||
continue
|
||||
}
|
||||
var key types.JID
|
||||
|
|
@ -332,7 +321,6 @@ func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *
|
|||
}
|
||||
messagesToRead[key] = append(messagesToRead[key], parsed.ID)
|
||||
}
|
||||
defer wa.mcTrack(receipt, time.Now(), &retErr)
|
||||
for messageSender, ids := range messagesToRead {
|
||||
err = wa.Client.MarkRead(ctx, ids, receipt.Receipt.Timestamp, portalJID, messageSender)
|
||||
if err != nil {
|
||||
|
|
@ -342,7 +330,7 @@ func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *
|
|||
return err
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.MatrixTyping) (retErr error) {
|
||||
func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.MatrixTyping) error {
|
||||
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -363,7 +351,6 @@ func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.
|
|||
return nil
|
||||
}
|
||||
|
||||
defer wa.mcTrack(msg, time.Now(), &retErr)
|
||||
if wa.Main.Config.SendPresenceOnTyping {
|
||||
err = wa.updatePresence(ctx, types.PresenceAvailable)
|
||||
if err != nil {
|
||||
|
|
@ -375,7 +362,7 @@ func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.
|
|||
|
||||
var errUnsupportedDisappearingTimer = bridgev2.WrapErrorInStatus(errors.New("invalid value for disappearing timer")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true)
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg *bridgev2.MatrixDisappearingTimer) (ok bool, retErr error) {
|
||||
func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg *bridgev2.MatrixDisappearingTimer) (bool, error) {
|
||||
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
|
@ -387,7 +374,6 @@ func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg
|
|||
return false, fmt.Errorf("%w (%s)", errUnsupportedDisappearingTimer, msg.Content.Timer.Duration)
|
||||
}
|
||||
|
||||
defer wa.mcTrack(msg, time.Now(), &retErr)
|
||||
settingTS := time.UnixMilli(msg.Event.Timestamp)
|
||||
err = wa.Client.SetDisappearingTimer(ctx, portalJID, msg.Content.Timer.Duration, settingTS)
|
||||
if err != nil {
|
||||
|
|
@ -404,11 +390,7 @@ func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg
|
|||
return true, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (result *bridgev2.MatrixMembershipResult, retErr error) {
|
||||
if msg.Type.IsSelf && msg.OrigSender != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (*bridgev2.MatrixMembershipResult, error) {
|
||||
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -448,7 +430,6 @@ func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridg
|
|||
return nil, fmt.Errorf("cannot get target intent: unknown type: %T", target)
|
||||
}
|
||||
|
||||
defer wa.mcTrack(msg, time.Now(), &retErr)
|
||||
resp, err := wa.Client.UpdateGroupParticipants(ctx, portalJID, changes, action)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -464,7 +445,7 @@ func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridg
|
|||
return &bridgev2.MatrixMembershipResult{RedirectTo: waid.MakeUserID(resp[0].JID)}, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev2.MatrixRoomName) (ok bool, retErr error) {
|
||||
func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev2.MatrixRoomName) (bool, error) {
|
||||
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
|
@ -474,7 +455,6 @@ func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev
|
|||
return false, fmt.Errorf("cannot set room name for DM")
|
||||
}
|
||||
|
||||
defer wa.mcTrack(msg, time.Now(), &retErr)
|
||||
err = wa.Client.SetGroupName(ctx, portalJID, msg.Content.Name)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
|
@ -486,7 +466,7 @@ func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev
|
|||
return true, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridgev2.MatrixRoomTopic) (ok bool, retErr error) {
|
||||
func (wa *WhatsAppClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridgev2.MatrixRoomTopic) (bool, error) {
|
||||
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
|
@ -496,7 +476,6 @@ func (wa *WhatsAppClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridge
|
|||
return false, fmt.Errorf("cannot set room topic for DM")
|
||||
}
|
||||
|
||||
defer wa.mcTrack(msg, time.Now(), &retErr)
|
||||
newID := wa.Client.GenerateMessageID()
|
||||
oldID := msg.Portal.Metadata.(*waid.PortalMetadata).TopicID
|
||||
err = wa.Client.SetGroupTopic(ctx, portalJID, oldID, newID, msg.Content.Topic)
|
||||
|
|
@ -511,7 +490,7 @@ func (wa *WhatsAppClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridge
|
|||
return true, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridgev2.MatrixRoomAvatar) (ok bool, retErr error) {
|
||||
func (wa *WhatsAppClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridgev2.MatrixRoomAvatar) (bool, error) {
|
||||
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
|
@ -534,7 +513,6 @@ func (wa *WhatsAppClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridg
|
|||
}
|
||||
}
|
||||
|
||||
defer wa.mcTrack(msg, time.Now(), &retErr)
|
||||
avatarID, err := wa.Client.SetGroupPhoto(ctx, portalJID, data)
|
||||
if err != nil {
|
||||
return false, err
|
||||
|
|
@ -602,7 +580,7 @@ func convertRoomAvatar(data []byte) ([]byte, error) {
|
|||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMute(ctx context.Context, msg *bridgev2.MatrixMute) (retErr error) {
|
||||
func (wa *WhatsAppClient) HandleMute(ctx context.Context, msg *bridgev2.MatrixMute) error {
|
||||
chatJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -613,16 +591,14 @@ func (wa *WhatsAppClient) HandleMute(ctx context.Context, msg *bridgev2.MatrixMu
|
|||
if !muted || mutedUntil == event.MutedForever {
|
||||
muteTS = nil
|
||||
}
|
||||
defer wa.mcTrack(msg, time.Now(), &retErr)
|
||||
return wa.Client.SendAppState(ctx, appstate.BuildMuteAbs(chatJID, muted, muteTS))
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleRoomTag(ctx context.Context, msg *bridgev2.MatrixRoomTag) (retErr error) {
|
||||
func (wa *WhatsAppClient) HandleRoomTag(ctx context.Context, msg *bridgev2.MatrixRoomTag) error {
|
||||
chatJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer wa.mcTrack(msg, time.Now(), &retErr)
|
||||
_, isFavorite := msg.Content.Tags[event.RoomTagFavourite]
|
||||
return wa.Client.SendAppState(ctx, appstate.BuildPin(chatJID, isFavorite))
|
||||
}
|
||||
|
|
@ -638,7 +614,7 @@ func (wa *WhatsAppClient) getLastMessageInfo(ctx context.Context, chatJID types.
|
|||
lastTS = msgs[0].Timestamp
|
||||
parsed, _ := waid.ParseMessageID(msgs[0].ID)
|
||||
if parsed != nil {
|
||||
fromMe := wa.IsOwnJID(parsed.Sender)
|
||||
fromMe := parsed.Sender.ToNonAD() == wa.JID.ToNonAD() || parsed.Sender.ToNonAD() == wa.GetStore().GetLID().ToNonAD()
|
||||
var participant *string
|
||||
if chatJID.Server == types.GroupServer {
|
||||
participant = ptr.Ptr(parsed.Sender.String())
|
||||
|
|
@ -654,7 +630,7 @@ func (wa *WhatsAppClient) getLastMessageInfo(ctx context.Context, chatJID types.
|
|||
return lastTS, lastKey, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMarkedUnread(ctx context.Context, msg *bridgev2.MatrixMarkedUnread) (retErr error) {
|
||||
func (wa *WhatsAppClient) HandleMarkedUnread(ctx context.Context, msg *bridgev2.MatrixMarkedUnread) error {
|
||||
chatJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -663,11 +639,10 @@ func (wa *WhatsAppClient) HandleMarkedUnread(ctx context.Context, msg *bridgev2.
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer wa.mcTrack(msg, time.Now(), &retErr)
|
||||
return wa.Client.SendAppState(ctx, appstate.BuildMarkChatAsRead(chatJID, msg.Content.Unread, lastTS, lastKey))
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridgev2.MatrixDeleteChat) (retErr error) {
|
||||
func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridgev2.MatrixDeleteChat) error {
|
||||
chatJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
|
|
@ -688,6 +663,5 @@ func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridg
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer wa.mcTrack(msg, time.Now(), &retErr)
|
||||
return wa.Client.SendAppState(ctx, appstate.BuildDeleteChat(chatJID, lastTS, lastKey, true))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ import (
|
|||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/appstate"
|
||||
"go.mau.fi/whatsmeow/proto/waE2E"
|
||||
"go.mau.fi/whatsmeow/store"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"go.mau.fi/whatsmeow/types/events"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
|
|
@ -76,7 +77,6 @@ func init() {
|
|||
func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
|
||||
log := wa.UserLogin.Log
|
||||
ctx := log.WithContext(wa.Main.Bridge.BackgroundCtx)
|
||||
wa.MC.OnWhatsAppEvent(rawEvt)
|
||||
|
||||
success = true
|
||||
switch evt := rawEvt.(type) {
|
||||
|
|
@ -113,7 +113,8 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
|
|||
case *events.HistorySync:
|
||||
wa.UserLogin.Log.Warn().Msg("Unexpected history sync event received")
|
||||
case *events.MediaRetry:
|
||||
success = wa.handleWAMediaRetry(ctx, evt)
|
||||
wa.phoneSeen(evt.Timestamp)
|
||||
success = wa.UserLogin.QueueRemoteEvent(&WAMediaRetry{MediaRetry: evt, wa: wa}).Success
|
||||
|
||||
case *events.GroupInfo:
|
||||
success = wa.handleWAGroupInfoChange(ctx, evt)
|
||||
|
|
@ -125,20 +126,6 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
|
|||
success = wa.handleWANewsletterLeave(evt)
|
||||
case *events.Picture:
|
||||
success = wa.handleWAPictureUpdate(ctx, evt)
|
||||
case *events.NotifyAccountReachoutTimelock:
|
||||
wa.UserLogin.TrackAnalytics("WhatsApp Account Reachout Timelock", map[string]any{
|
||||
"enforcement_type": evt.EnforcementType,
|
||||
"is_active": evt.IsActive,
|
||||
"time_enforcement_ends": evt.TimeEnforcementEnds.Time,
|
||||
})
|
||||
wa.UserLogin.Metadata.(*waid.UserLoginMetadata).ReachoutTimelockUntil = evt.TimeEnforcementEnds.Time
|
||||
if wa.UserLogin.BridgeState.GetPrevUnsent().StateEvent == status.StateConnected {
|
||||
wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
|
||||
}
|
||||
err := wa.UserLogin.Save(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to save user login metadata after reachout timelock update")
|
||||
}
|
||||
|
||||
case *events.AppStateSyncComplete:
|
||||
wa.handleWAAppStateSyncComplete(ctx, evt)
|
||||
|
|
@ -157,7 +144,7 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
|
|||
if err != nil {
|
||||
log.Err(err).Msg("Failed to update push name in store")
|
||||
}
|
||||
_, _, err = wa.GetStore().Contacts.PutPushName(ctx, wa.GetLID().ToNonAD(), evt.Action.GetName())
|
||||
_, _, err = wa.GetStore().Contacts.PutPushName(ctx, wa.GetStore().GetLID().ToNonAD(), evt.Action.GetName())
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to update push name in store")
|
||||
}
|
||||
|
|
@ -181,6 +168,7 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
|
|||
}()
|
||||
go wa.syncRemoteProfile(ctx, nil)
|
||||
}
|
||||
wa.MC.OnConnect(store.GetWAVersion()[2], wa.Device.Platform)
|
||||
case *events.OfflineSyncPreview:
|
||||
log.Info().
|
||||
Int("message_count", evt.Messages).
|
||||
|
|
@ -258,40 +246,115 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
|
|||
return
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) ensureAltJIDs(ctx context.Context, info *types.MessageSource, checkPhones bool) bool {
|
||||
var err error
|
||||
if info.Sender.Server == types.DefaultUserServer && info.SenderAlt.IsEmpty() {
|
||||
info.SenderAlt, err = wa.GetStore().LIDs.GetLIDForPN(ctx, info.Sender)
|
||||
func (wa *WhatsAppClient) rerouteWAMessage(ctx context.Context, evtType string, info *types.MessageSource, msgID any) {
|
||||
if (info.Chat.Server == types.HiddenUserServer || info.Chat.Server == types.BroadcastServer) &&
|
||||
info.Sender.Server == types.HiddenUserServer && info.SenderAlt.IsEmpty() {
|
||||
info.SenderAlt, _ = wa.GetStore().LIDs.GetPNForLID(ctx, info.Sender)
|
||||
}
|
||||
if info.Chat.Server == types.HiddenUserServer && info.IsFromMe && info.RecipientAlt.IsEmpty() {
|
||||
info.RecipientAlt, _ = wa.GetStore().LIDs.GetPNForLID(ctx, info.Chat)
|
||||
}
|
||||
if info.Chat.Server == types.HiddenUserServer && info.Sender.ToNonAD() == info.Chat && info.SenderAlt.Server == types.DefaultUserServer {
|
||||
wa.UserLogin.Log.Debug().
|
||||
Stringer("lid", info.Sender).
|
||||
Stringer("pn", info.SenderAlt).
|
||||
Any("message_id", msgID).
|
||||
Str("evt_type", evtType).
|
||||
Msg("Forced LID DM sender to phone number in incoming message")
|
||||
info.Sender, info.SenderAlt = info.SenderAlt, info.Sender
|
||||
info.Chat = info.Sender.ToNonAD()
|
||||
} else if info.Chat.Server == types.HiddenUserServer && info.IsFromMe && info.RecipientAlt.Server == types.DefaultUserServer {
|
||||
wa.UserLogin.Log.Debug().
|
||||
Stringer("lid", info.Chat).
|
||||
Stringer("pn", info.RecipientAlt).
|
||||
Any("message_id", msgID).
|
||||
Str("evt_type", evtType).
|
||||
Msg("Forced LID DM sender to phone number in own message sent from another device")
|
||||
info.Chat = info.RecipientAlt.ToNonAD()
|
||||
if info.Sender.Server == types.HiddenUserServer {
|
||||
info.Sender, info.SenderAlt = info.SenderAlt, info.Sender
|
||||
if info.Sender.IsEmpty() {
|
||||
info.Sender = wa.GetStore().GetJID()
|
||||
info.Sender.Device = info.SenderAlt.Device
|
||||
}
|
||||
}
|
||||
} else if info.Chat.Server == types.BroadcastServer && info.Sender.Server == types.HiddenUserServer && info.SenderAlt.Server == types.DefaultUserServer {
|
||||
wa.UserLogin.Log.Debug().
|
||||
Stringer("lid", info.Sender).
|
||||
Stringer("pn", info.SenderAlt).
|
||||
Stringer("chat", info.Chat).
|
||||
Any("message_id", msgID).
|
||||
Str("evt_type", evtType).
|
||||
Msg("Forced LID broadcast list sender to phone number in incoming message")
|
||||
info.Sender, info.SenderAlt = info.SenderAlt, info.Sender
|
||||
} else if info.Sender.Server == types.BotServer && info.Chat.Server == types.HiddenUserServer {
|
||||
chatPN, err := wa.GetStore().LIDs.GetPNForLID(ctx, info.Chat)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Stringer("sender", info.Sender).Msg("Failed to get LID for sender")
|
||||
return false
|
||||
wa.UserLogin.Log.Err(err).
|
||||
Any("message_id", msgID).
|
||||
Stringer("lid", info.Chat).
|
||||
Str("evt_type", evtType).
|
||||
Msg("Failed to get phone number of DM for incoming bot message")
|
||||
} else if !chatPN.IsEmpty() {
|
||||
wa.UserLogin.Log.Debug().
|
||||
Stringer("lid", info.Chat).
|
||||
Stringer("pn", chatPN).
|
||||
Any("message_id", msgID).
|
||||
Str("evt_type", evtType).
|
||||
Msg("Forced LID chat to phone number in bot message")
|
||||
info.Chat = chatPN
|
||||
}
|
||||
}
|
||||
if info.Chat.Server == types.DefaultUserServer && info.IsFromMe && info.RecipientAlt.IsEmpty() {
|
||||
info.RecipientAlt, err = wa.GetStore().LIDs.GetLIDForPN(ctx, info.Chat)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Stringer("chat", info.Chat).Msg("Failed to get LID for chat")
|
||||
return false
|
||||
}
|
||||
}
|
||||
if checkPhones {
|
||||
return wa.checkAllPhonesInMessage(ctx, info)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Message) (success bool) {
|
||||
success = true
|
||||
wa.rerouteWAMessage(ctx, "message", &evt.Info.MessageSource, evt.Info.ID)
|
||||
wa.UserLogin.Log.Trace().
|
||||
Any("info", evt.Info).
|
||||
Any("payload", evt.Message).
|
||||
Msg("Received WhatsApp message")
|
||||
if evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast {
|
||||
return
|
||||
}
|
||||
if evt.Info.Chat.Server == types.NewsletterServer && wa.disableNewsletter {
|
||||
if evt.Info.IsFromMe &&
|
||||
evt.Message.GetProtocolMessage().GetHistorySyncNotification() != nil &&
|
||||
wa.Main.Bridge.Config.Backfill.Enabled &&
|
||||
wa.Client.ManualHistorySyncDownload {
|
||||
wa.saveWAHistorySyncNotification(ctx, evt.Message.ProtocolMessage.HistorySyncNotification)
|
||||
}
|
||||
|
||||
messageAssoc := evt.Message.GetMessageContextInfo().GetMessageAssociation()
|
||||
if assocType := messageAssoc.GetAssociationType(); assocType == waE2E.MessageAssociation_HD_IMAGE_DUAL_UPLOAD || assocType == waE2E.MessageAssociation_HD_VIDEO_DUAL_UPLOAD {
|
||||
parentKey := messageAssoc.GetParentMessageKey()
|
||||
associatedMessage := evt.Message.GetAssociatedChildMessage().GetMessage()
|
||||
wa.UserLogin.Log.Debug().
|
||||
Str("message_id", evt.Info.ID).
|
||||
Str("parent_id", parentKey.GetID()).
|
||||
Stringer("assoc_type", assocType).
|
||||
Msg("Received HD replacement message, converting to edit")
|
||||
|
||||
protocolMsg := &waE2E.ProtocolMessage{
|
||||
Type: waE2E.ProtocolMessage_MESSAGE_EDIT.Enum(),
|
||||
Key: parentKey,
|
||||
EditedMessage: associatedMessage,
|
||||
}
|
||||
evt.Message = &waE2E.Message{
|
||||
ProtocolMessage: protocolMsg,
|
||||
}
|
||||
} else if assocType == waE2E.MessageAssociation_MOTION_PHOTO {
|
||||
//evt.Message = evt.Message.GetAssociatedChildMessage().GetMessage()
|
||||
wa.UserLogin.Log.Debug().
|
||||
Str("message_id", evt.Info.ID).
|
||||
Str("parent_id", messageAssoc.GetParentMessageKey().GetID()).
|
||||
Msg("Ignoring motion photo update")
|
||||
return
|
||||
}
|
||||
if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, true) {
|
||||
return false
|
||||
}
|
||||
|
||||
parsedMessageType := getMessageType(evt.Message)
|
||||
if parsedMessageType == "ignore" || strings.HasPrefix(parsedMessageType, "unknown_protocol_") {
|
||||
return
|
||||
}
|
||||
if encReact := evt.Message.GetEncReactionMessage(); encReact != nil {
|
||||
decrypted, err := wa.Client.DecryptReaction(ctx, evt)
|
||||
if err != nil {
|
||||
|
|
@ -313,65 +376,13 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
|
|||
if encMessage := evt.Message.GetSecretEncryptedMessage(); encMessage != nil {
|
||||
decrypted, err := wa.Client.DecryptSecretEncryptedMessage(ctx, evt)
|
||||
if err != nil {
|
||||
wa.UserLogin.Log.Err(err).
|
||||
Str("message_id", evt.Info.ID).
|
||||
Stringer("evt_sender", evt.Info.Sender).
|
||||
Any("target_message_key", encMessage.TargetMessageKey).
|
||||
Msg("Failed to decrypt secret-encrypted message")
|
||||
wa.UserLogin.Log.Err(err).Str("message_id", evt.Info.ID).Msg("Failed to decrypt message")
|
||||
return
|
||||
}
|
||||
evt.RawMessage = decrypted
|
||||
evt.UnwrapRaw()
|
||||
parsedMessageType = getMessageType(evt.Message)
|
||||
}
|
||||
wa.UserLogin.Log.Trace().
|
||||
Any("info", evt.Info).
|
||||
Any("payload", evt.Message).
|
||||
Msg("Received WhatsApp message")
|
||||
if evt.Info.IsFromMe &&
|
||||
evt.Message.GetProtocolMessage().GetHistorySyncNotification() != nil &&
|
||||
wa.Main.Bridge.Config.Backfill.Enabled {
|
||||
wa.saveWAHistorySyncNotification(ctx, evt.Message.ProtocolMessage.HistorySyncNotification)
|
||||
}
|
||||
if parsedMessageType == "ignore" || strings.HasPrefix(parsedMessageType, "unknown_protocol_") {
|
||||
return
|
||||
}
|
||||
|
||||
dontRenderEdited := false
|
||||
messageAssoc := evt.Message.GetMessageContextInfo().GetMessageAssociation()
|
||||
if assocType := messageAssoc.GetAssociationType(); assocType == waE2E.MessageAssociation_HD_IMAGE_DUAL_UPLOAD || assocType == waE2E.MessageAssociation_HD_VIDEO_DUAL_UPLOAD {
|
||||
parentKey := messageAssoc.GetParentMessageKey()
|
||||
protocolMsg := evt.Message.GetProtocolMessage()
|
||||
if protocolMsg.GetType() != waE2E.ProtocolMessage_MESSAGE_EDIT || protocolMsg.GetKey() == nil {
|
||||
protocolMsg = &waE2E.ProtocolMessage{
|
||||
Type: waE2E.ProtocolMessage_MESSAGE_EDIT.Enum(),
|
||||
Key: parentKey,
|
||||
EditedMessage: evt.Message.GetAssociatedChildMessage().GetMessage(),
|
||||
}
|
||||
dontRenderEdited = true
|
||||
} else if child := protocolMsg.GetEditedMessage().GetAssociatedChildMessage().GetMessage(); child != nil {
|
||||
protocolMsg.EditedMessage = child
|
||||
protocolMsg.Key = parentKey
|
||||
}
|
||||
wa.UserLogin.Log.Debug().
|
||||
Str("message_id", evt.Info.ID).
|
||||
Str("parent_id", parentKey.GetID()).
|
||||
Stringer("assoc_type", assocType).
|
||||
Msg("Received HD replacement message, converting to edit")
|
||||
|
||||
evt.Message = &waE2E.Message{
|
||||
ProtocolMessage: protocolMsg,
|
||||
}
|
||||
parsedMessageType = getMessageType(evt.Message)
|
||||
} else if assocType == waE2E.MessageAssociation_MOTION_PHOTO {
|
||||
//evt.Message = evt.Message.GetAssociatedChildMessage().GetMessage()
|
||||
wa.UserLogin.Log.Debug().
|
||||
Str("message_id", evt.Info.ID).
|
||||
Str("parent_id", messageAssoc.GetParentMessageKey().GetID()).
|
||||
Msg("Ignoring motion photo update")
|
||||
return
|
||||
}
|
||||
|
||||
res := wa.UserLogin.QueueRemoteEvent(&WAMessageEvent{
|
||||
MessageInfoWrapper: &MessageInfoWrapper{
|
||||
Info: evt.Info,
|
||||
|
|
@ -381,15 +392,12 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
|
|||
MsgEvent: evt,
|
||||
|
||||
parsedMessageType: parsedMessageType,
|
||||
dontRenderEdited: dontRenderEdited,
|
||||
})
|
||||
return res.Success
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWAUndecryptableMessage(ctx context.Context, evt *events.UndecryptableMessage) bool {
|
||||
if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, true) {
|
||||
return false
|
||||
}
|
||||
wa.rerouteWAMessage(ctx, "undecryptable message", &evt.Info.MessageSource, evt.Info.ID)
|
||||
wa.UserLogin.Log.Debug().
|
||||
Any("info", evt.Info).
|
||||
Bool("unavailable", evt.IsUnavailable).
|
||||
|
|
@ -412,44 +420,12 @@ func (wa *WhatsAppClient) handleWAUndecryptableMessage(ctx context.Context, evt
|
|||
return res.Success
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWAMediaRetry(ctx context.Context, evt *events.MediaRetry) bool {
|
||||
wa.phoneSeen(evt.Timestamp)
|
||||
var senderLID, chatLID types.JID
|
||||
var err error
|
||||
if evt.SenderID.Server == types.DefaultUserServer {
|
||||
senderLID, err = wa.GetStore().LIDs.GetLIDForPN(ctx, evt.SenderID)
|
||||
if err != nil {
|
||||
wa.UserLogin.Log.Err(err).
|
||||
Stringer("sender_id", evt.SenderID).
|
||||
Msg("Failed to get LID for media retry sender")
|
||||
return false
|
||||
}
|
||||
}
|
||||
if evt.ChatID.Server == types.DefaultUserServer {
|
||||
chatLID, err = wa.GetStore().LIDs.GetLIDForPN(ctx, evt.ChatID)
|
||||
if err != nil {
|
||||
wa.UserLogin.Log.Err(err).
|
||||
Stringer("chat_id", evt.ChatID).
|
||||
Msg("Failed to get LID for media retry chat")
|
||||
return false
|
||||
}
|
||||
}
|
||||
res := wa.UserLogin.QueueRemoteEvent(&WAMediaRetry{
|
||||
MediaRetry: evt,
|
||||
wa: wa,
|
||||
senderLID: senderLID,
|
||||
chatLID: chatLID,
|
||||
})
|
||||
return res.Success
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Receipt) (success bool) {
|
||||
origChat := evt.Chat
|
||||
wa.rerouteWAMessage(ctx, "receipt", &evt.MessageSource, evt.MessageIDs)
|
||||
if evt.IsFromMe && evt.Sender.Device == 0 {
|
||||
wa.phoneSeen(evt.Timestamp)
|
||||
}
|
||||
if !wa.ensureAltJIDs(ctx, &evt.MessageSource, true) {
|
||||
return false
|
||||
}
|
||||
var evtType bridgev2.RemoteEventType
|
||||
switch evt.Type {
|
||||
case types.ReceiptTypeRead, types.ReceiptTypeReadSelf:
|
||||
|
|
@ -461,34 +437,28 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei
|
|||
default:
|
||||
return true
|
||||
}
|
||||
targets := make([]networkid.MessageID, 0, len(evt.MessageIDs))
|
||||
messageSender := wa.GetLID()
|
||||
targets := make([]networkid.MessageID, len(evt.MessageIDs))
|
||||
messageSender := wa.JID
|
||||
if !evt.MessageSender.IsEmpty() {
|
||||
messageSender = evt.MessageSender
|
||||
// Second part of rerouting receipts in LID chats
|
||||
if messageSender == origChat && evt.Chat != origChat {
|
||||
messageSender = evt.Chat
|
||||
}
|
||||
var chatAlt types.JID
|
||||
if evt.Chat.Server == types.DefaultUserServer {
|
||||
chatLID, _ := wa.GetStore().LIDs.GetLIDForPN(ctx, evt.Chat)
|
||||
if !chatLID.IsEmpty() {
|
||||
chatAlt = evt.Chat
|
||||
evt.Chat = chatLID
|
||||
} else if evt.Chat.Server == types.GroupServer && evt.Sender.Server == types.HiddenUserServer {
|
||||
lid := wa.GetStore().GetLID()
|
||||
if !lid.IsEmpty() {
|
||||
messageSender = lid
|
||||
}
|
||||
}
|
||||
for _, id := range evt.MessageIDs {
|
||||
targets = append(targets, waid.MakeMessageID(evt.Chat, messageSender, id))
|
||||
if !chatAlt.IsEmpty() {
|
||||
targets = append(targets, waid.MakeMessageID(chatAlt, messageSender, id))
|
||||
}
|
||||
}
|
||||
senderLID := evt.Sender
|
||||
if senderLID.Server == types.DefaultUserServer && !evt.SenderAlt.IsEmpty() {
|
||||
senderLID = evt.SenderAlt
|
||||
for i, id := range evt.MessageIDs {
|
||||
targets[i] = waid.MakeMessageID(evt.Chat, messageSender, id)
|
||||
}
|
||||
res := wa.UserLogin.QueueRemoteEvent(&simplevent.Receipt{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: evtType,
|
||||
PortalKey: wa.makeWAPortalKey(evt.Chat),
|
||||
Sender: wa.makeEventSender(ctx, senderLID),
|
||||
Sender: wa.makeEventSender(ctx, evt.Sender),
|
||||
Timestamp: evt.Timestamp,
|
||||
},
|
||||
Targets: targets,
|
||||
|
|
@ -497,11 +467,11 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei
|
|||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWAChatPresence(ctx context.Context, evt *events.ChatPresence) {
|
||||
if evt.Chat.Server == types.DefaultUserServer && evt.Sender.ToNonAD() == evt.Chat {
|
||||
if evt.Chat.Server == types.HiddenUserServer && evt.Sender.ToNonAD() == evt.Chat {
|
||||
if evt.SenderAlt.IsEmpty() {
|
||||
evt.SenderAlt, _ = wa.GetStore().LIDs.GetLIDForPN(ctx, evt.Sender)
|
||||
evt.SenderAlt, _ = wa.GetStore().LIDs.GetPNForLID(ctx, evt.Sender)
|
||||
}
|
||||
if evt.SenderAlt.Server == types.HiddenUserServer {
|
||||
if evt.SenderAlt.Server == types.DefaultUserServer {
|
||||
evt.Sender, evt.SenderAlt = evt.SenderAlt, evt.Sender
|
||||
evt.Chat = evt.Sender.ToNonAD()
|
||||
}
|
||||
|
|
@ -538,7 +508,6 @@ func (wa *WhatsAppClient) handleWALogout(reason events.ConnectFailureReason, onC
|
|||
wa.Disconnect()
|
||||
wa.Client = nil
|
||||
wa.JID = types.EmptyJID
|
||||
wa.LID = types.EmptyJID
|
||||
wa.UserLogin.Metadata.(*waid.UserLoginMetadata).WADeviceID = 0
|
||||
wa.UserLogin.BridgeState.Send(status.BridgeState{
|
||||
StateEvent: status.StateBadCredentials,
|
||||
|
|
@ -552,15 +521,12 @@ func (wa *WhatsAppClient) handleWACallStart(ctx context.Context, group, sender,
|
|||
if !wa.Main.Config.CallStartNotices || time.Since(ts) > callEventMaxAge {
|
||||
return true
|
||||
}
|
||||
if sender.Server == types.DefaultUserServer && senderAlt.IsEmpty() {
|
||||
senderAlt, _ = wa.GetStore().LIDs.GetLIDForPN(ctx, sender)
|
||||
}
|
||||
if sender.Server == types.DefaultUserServer && senderAlt.Server == types.HiddenUserServer {
|
||||
if sender.Server == types.HiddenUserServer && senderAlt.Server == types.DefaultUserServer {
|
||||
wa.UserLogin.Log.Debug().
|
||||
Stringer("lid", senderAlt).
|
||||
Stringer("pn", sender).
|
||||
Stringer("lid", sender).
|
||||
Stringer("pn", senderAlt).
|
||||
Str("call_id", id).
|
||||
Msg("Forced phone number caller to LID in incoming call")
|
||||
Msg("Forced LID caller to phone number in incoming call")
|
||||
sender, senderAlt = senderAlt, sender
|
||||
}
|
||||
chat := group
|
||||
|
|
@ -607,12 +573,6 @@ func (wa *WhatsAppClient) handleWAIdentityChange(ctx context.Context, evt *event
|
|||
if !wa.Main.Config.IdentityChangeNotices {
|
||||
return
|
||||
}
|
||||
if evt.JID.Server == types.DefaultUserServer {
|
||||
lid, _ := wa.GetStore().LIDs.GetLIDForPN(ctx, evt.JID)
|
||||
if !lid.IsEmpty() {
|
||||
evt.JID = lid
|
||||
}
|
||||
}
|
||||
wa.UserLogin.QueueRemoteEvent(&simplevent.Message[*events.IdentityChange]{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventMessage,
|
||||
|
|
@ -663,14 +623,13 @@ func (wa *WhatsAppClient) handleWADeleteChat(ctx context.Context, evt *events.De
|
|||
|
||||
func (wa *WhatsAppClient) handleWADeleteForMe(ctx context.Context, evt *events.DeleteForMe) bool {
|
||||
chatJID := wa.maybeConvertJIDToLID(ctx, evt.ChatJID)
|
||||
senderJID := wa.maybeConvertJIDToLID(ctx, evt.SenderJID)
|
||||
return wa.UserLogin.QueueRemoteEvent(&simplevent.MessageRemove{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventMessageRemove,
|
||||
PortalKey: wa.makeWAPortalKey(chatJID),
|
||||
Timestamp: evt.Timestamp,
|
||||
},
|
||||
TargetMessage: waid.MakeMessageID(chatJID, senderJID, evt.MessageID),
|
||||
TargetMessage: waid.MakeMessageID(chatJID, evt.SenderJID, evt.MessageID),
|
||||
OnlyForMe: true,
|
||||
}).Success
|
||||
}
|
||||
|
|
@ -681,7 +640,7 @@ func (wa *WhatsAppClient) handleWAMarkChatAsRead(ctx context.Context, evt *event
|
|||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventReadReceipt,
|
||||
PortalKey: wa.makeWAPortalKey(chatJID),
|
||||
Sender: wa.makeEventSender(ctx, wa.GetLID()),
|
||||
Sender: wa.makeEventSender(ctx, wa.JID),
|
||||
Timestamp: evt.Timestamp,
|
||||
},
|
||||
ReadUpTo: evt.Timestamp,
|
||||
|
|
@ -770,6 +729,9 @@ func (wa *WhatsAppClient) handleWAGroupInfoChange(ctx context.Context, evt *even
|
|||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWAJoinedGroup(ctx context.Context, evt *events.JoinedGroup) bool {
|
||||
if wa.createDedup.Pop(evt.CreateKey) {
|
||||
return true
|
||||
}
|
||||
return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventChatResync,
|
||||
|
|
@ -782,9 +744,6 @@ func (wa *WhatsAppClient) handleWAJoinedGroup(ctx context.Context, evt *events.J
|
|||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWANewsletterJoin(ctx context.Context, evt *events.NewsletterJoin) bool {
|
||||
if wa.disableNewsletter {
|
||||
return true
|
||||
}
|
||||
return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventChatResync,
|
||||
|
|
@ -883,12 +842,7 @@ func (wa *WhatsAppClient) handleWAAppStateSyncComplete(ctx context.Context, evt
|
|||
} else {
|
||||
log.Info().
|
||||
Time("recovery_ts", ts).
|
||||
Bool("recovery_evt", evt.Recovery).
|
||||
Msg("Unmarked app state recovery as attempted after successful full sync")
|
||||
wa.UserLogin.TrackAnalytics("WhatsApp Appstate Recovery Success", map[string]any{
|
||||
"patch_name": evt.Name,
|
||||
"from_recovery": evt.Recovery,
|
||||
})
|
||||
}
|
||||
} else if ts, exists = wa.appStateFullSyncAttempted[evt.Name]; exists {
|
||||
delete(wa.appStateFullSyncAttempted, evt.Name)
|
||||
|
|
@ -945,9 +899,6 @@ func (wa *WhatsAppClient) handleWAAppStateSyncError(ctx context.Context, evt *ev
|
|||
if err != nil {
|
||||
log.Err(err).Msg("Failed to save login metadata after marking app state recovery as attempted")
|
||||
}
|
||||
wa.UserLogin.TrackAnalytics("WhatsApp Appstate Recovery Request", map[string]any{
|
||||
"patch_name": evt.Name,
|
||||
})
|
||||
go func() {
|
||||
resp, err := wa.Client.SendPeerMessage(ctx, whatsmeow.BuildAppStateRecoveryRequest(evt.Name))
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ func (wa *WhatsAppClient) makeEventSender(ctx context.Context, id types.JID) bri
|
|||
}
|
||||
}
|
||||
return bridgev2.EventSender{
|
||||
IsFromMe: wa.IsOwnJID(id),
|
||||
IsFromMe: id.User == wa.GetStore().GetJID().User || id.User == wa.GetStore().GetLID().User,
|
||||
Sender: waid.MakeUserID(id),
|
||||
SenderLogin: waid.MakeUserLoginID(senderLoginJID),
|
||||
}
|
||||
|
|
@ -60,25 +60,24 @@ func (wa *WhatsAppClient) messageIDToKey(id *waid.ParsedMessageID) *waCommon.Mes
|
|||
RemoteJID: ptr.Ptr(id.Chat.String()),
|
||||
ID: ptr.Ptr(id.ID),
|
||||
}
|
||||
if wa.IsOwnJID(id.Sender) {
|
||||
if id.Sender.User == wa.GetStore().GetJID().User || id.Sender.User == wa.GetStore().GetLID().User {
|
||||
key.FromMe = ptr.Ptr(true)
|
||||
}
|
||||
if id.Chat.Server != types.MessengerServer && id.Chat.Server != types.DefaultUserServer &&
|
||||
id.Chat.Server != types.HiddenUserServer && id.Chat.Server != types.BotServer {
|
||||
if id.Chat.Server != types.MessengerServer && id.Chat.Server != types.DefaultUserServer && id.Chat.Server != types.HiddenUserServer && id.Chat.Server != types.BotServer {
|
||||
key.Participant = ptr.Ptr(id.Sender.String())
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) maybeConvertJIDToLID(ctx context.Context, jid types.JID) types.JID {
|
||||
if jid.Server == types.DefaultUserServer {
|
||||
if lidForPN, err := wa.GetStore().LIDs.GetLIDForPN(ctx, jid); err != nil {
|
||||
func (wa *WhatsAppClient) maybeConvertJIDToLID(ctx context.Context, chatJID types.JID) types.JID {
|
||||
if chatJID.Server == types.HiddenUserServer {
|
||||
if pn, err := wa.GetStore().LIDs.GetPNForLID(ctx, chatJID); err != nil {
|
||||
wa.UserLogin.Log.Err(err).
|
||||
Stringer("pn", jid).
|
||||
Msg("Failed to get LID for phone number chat")
|
||||
} else if !lidForPN.IsEmpty() {
|
||||
return lidForPN
|
||||
Stringer("lid", chatJID).
|
||||
Msg("Failed to get phone number for LID chat")
|
||||
} else if !pn.IsEmpty() {
|
||||
return pn.ToNonAD()
|
||||
}
|
||||
}
|
||||
return jid
|
||||
return chatJID
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,212 +0,0 @@
|
|||
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
|
||||
// Copyright (C) 2026 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package connector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/dbutil"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
"maunium.net/go/mautrix/event"
|
||||
|
||||
"go.mau.fi/mautrix-whatsapp/pkg/waid"
|
||||
)
|
||||
|
||||
func (wa *WhatsAppClient) FindAltTargetMessage(ctx context.Context, targetMsg networkid.MessageID, evt bridgev2.RemoteEventWithTargetMessage) (alts []networkid.MessageID, err error) {
|
||||
parsed, err := waid.ParseMessageID(targetMsg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse target message ID: %w", err)
|
||||
}
|
||||
altSender, err := wa.GetStore().GetAltJID(ctx, parsed.Sender)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var altChat types.JID
|
||||
if parsed.Chat.Server == types.HiddenUserServer {
|
||||
altChat, err = wa.GetStore().LIDs.GetPNForLID(ctx, parsed.Chat)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if !altSender.IsEmpty() {
|
||||
altSenderID := *parsed
|
||||
altSenderID.Sender = altSender
|
||||
alts = append(alts, altSenderID.String())
|
||||
}
|
||||
if !altChat.IsEmpty() {
|
||||
altChatID := *parsed
|
||||
altChatID.Chat = altChat
|
||||
if altSender.Server == types.DefaultUserServer {
|
||||
altChatID.Sender = altSender
|
||||
}
|
||||
alts = append(alts, altChatID.String())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) checkAllPhonesInMessage(ctx context.Context, info *types.MessageSource) (ok bool) {
|
||||
for _, jid := range []types.JID{info.Sender, info.SenderAlt, info.Chat, info.RecipientAlt, info.BroadcastListOwner} {
|
||||
if !wa.reIDPhoneDMToLIDIfNeeded(ctx, jid) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) reIDPhoneDMToLIDIfNeeded(ctx context.Context, pn types.JID) (ok bool) {
|
||||
if pn.Server != types.DefaultUserServer {
|
||||
return true
|
||||
}
|
||||
portalKey := wa.makeWAPortalKey(pn)
|
||||
if wa.Main.unmigratedDMs.Has(portalKey) {
|
||||
lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, pn)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Stringer("pn", pn).Msg("Failed to get LID for PN")
|
||||
return false
|
||||
} else if lid.IsEmpty() {
|
||||
zerolog.Ctx(ctx).Warn().Stringer("pn", pn).Msg("No found LID for phone number")
|
||||
return true
|
||||
}
|
||||
zerolog.Ctx(ctx).Info().
|
||||
Object("portal_key", portalKey).
|
||||
Stringer("pn", pn).
|
||||
Stringer("lid", lid).
|
||||
Msg("Received event for portal in unmigrated DMs list, trying migration")
|
||||
_, err = wa.Main.reIDPhoneDMToLID(ctx, pn, lid, wa.UserLogin.ID)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to re-ID phone DM to LID")
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (wa *WhatsAppConnector) reIDPhoneDMToLID(ctx context.Context, pn, lid types.JID, receiver networkid.UserLoginID) (bridgev2.ReIDResult, error) {
|
||||
pnKey := networkid.PortalKey{
|
||||
ID: waid.MakePortalID(pn),
|
||||
Receiver: receiver,
|
||||
}
|
||||
lidKey := networkid.PortalKey{
|
||||
ID: waid.MakePortalID(lid),
|
||||
Receiver: receiver,
|
||||
}
|
||||
result, portal, err := wa.Bridge.ReIDPortal(ctx, pnKey, lidKey)
|
||||
if err != nil {
|
||||
return result, err
|
||||
}
|
||||
if result == bridgev2.ReIDResultSourceReIDd || result == bridgev2.ReIDResultTargetDeletedAndSourceReIDd {
|
||||
var pnGhost, lidGhost *bridgev2.Ghost
|
||||
pnGhost, err = wa.Bridge.GetGhostByID(ctx, waid.MakeUserID(pn))
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("failed to get PN ghost: %w", err)
|
||||
}
|
||||
lidGhost, err = wa.Bridge.GetGhostByID(ctx, waid.MakeUserID(lid))
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("failed to get LID ghost: %w", err)
|
||||
}
|
||||
_, err = pnGhost.Intent.SendState(ctx, portal.MXID, event.StateMember, pnGhost.Intent.GetMXID().String(), &event.Content{
|
||||
Parsed: &event.MemberEventContent{Membership: event.MembershipLeave, Reason: "Migrating to LIDs"},
|
||||
Raw: map[string]any{"com.beeper.exclude_from_timeline": true},
|
||||
}, time.Time{})
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("failed to send leave event for PN ghost: %w", err)
|
||||
}
|
||||
_, err = wa.Bridge.Bot.SendState(ctx, portal.MXID, event.StateMember, lidGhost.Intent.GetMXID().String(), &event.Content{
|
||||
Parsed: &event.MemberEventContent{Membership: event.MembershipInvite, Reason: "Migrating to LIDs"},
|
||||
Raw: map[string]any{"com.beeper.exclude_from_timeline": true},
|
||||
}, time.Time{})
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("failed to send invite event for LID ghost: %w", err)
|
||||
}
|
||||
_, err = lidGhost.Intent.SendState(ctx, portal.MXID, event.StateMember, lidGhost.Intent.GetMXID().String(), &event.Content{
|
||||
Parsed: &event.MemberEventContent{Membership: event.MembershipJoin, Reason: "Migrating to LIDs"},
|
||||
Raw: map[string]any{"com.beeper.exclude_from_timeline": true},
|
||||
}, time.Time{})
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("failed to send join event for LID ghost: %w", err)
|
||||
}
|
||||
portal.OtherUserID = lidGhost.ID
|
||||
err = portal.Save(ctx)
|
||||
if err != nil {
|
||||
return result, fmt.Errorf("failed to save portal after re-ID: %w", err)
|
||||
}
|
||||
portal.UpdateBridgeInfo(ctx)
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var scanPortalKey = dbutil.ConvertRowFn[networkid.PortalKey](func(row dbutil.Scannable) (key networkid.PortalKey, err error) {
|
||||
err = row.Scan(&key.ID, &key.Receiver)
|
||||
return
|
||||
})
|
||||
|
||||
func (wa *WhatsAppConnector) migrateToLIDDMs(ctx context.Context) error {
|
||||
if wa.Bridge.Background {
|
||||
if wa.Bridge.DB.KV.Get(ctx, "whatsapp_lid_dms_migrated") == "true" {
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("can't migrate to LID DMs in background mode")
|
||||
}
|
||||
log := zerolog.Ctx(ctx).With().Str("action", "migrate to lid dms").Logger()
|
||||
const findPNPortals = "SELECT id, receiver FROM portal WHERE bridge_id=$1 AND room_type='dm' AND id LIKE '%@s.whatsapp.net'"
|
||||
pnPortalKeys, err := scanPortalKey.NewRowIter(wa.Bridge.DB.Query(ctx, findPNPortals, wa.Bridge.ID)).AsList()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get phone number portals: %w", err)
|
||||
}
|
||||
var updatedPortals, missingLID int
|
||||
for _, key := range pnPortalKeys {
|
||||
pnJID, err := waid.ParsePortalID(key.ID)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("portal_id", string(key.ID)).Msg("Failed to parse portal ID")
|
||||
continue
|
||||
} else if pnJID.Server != types.DefaultUserServer {
|
||||
continue
|
||||
}
|
||||
lid, err := wa.DeviceStore.LIDMap.GetLIDForPN(ctx, pnJID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get LID for PN portal %s: %w", key.ID, err)
|
||||
} else if lid.IsEmpty() {
|
||||
log.Warn().Stringer("pn", pnJID).Msg("No LID for PN portal")
|
||||
wa.unmigratedDMs.Add(key)
|
||||
missingLID++
|
||||
continue
|
||||
}
|
||||
res, err := wa.reIDPhoneDMToLID(ctx, pnJID, lid, key.Receiver)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to re-ID %s to %s: %w", pnJID, lid, err)
|
||||
}
|
||||
updatedPortals++
|
||||
log.Info().
|
||||
Stringer("pn", pnJID).
|
||||
Stringer("lid", lid).
|
||||
Stringer("result", res).
|
||||
Msg("Re-ID'd phone number DM portal")
|
||||
}
|
||||
log.Info().
|
||||
Int("updated_portals", updatedPortals).
|
||||
Int("total_pn_portals", len(pnPortalKeys)).
|
||||
Int("missing_lid", missingLID).
|
||||
Msg("Finished re-IDing phone number DM portals")
|
||||
wa.Bridge.DB.KV.Set(ctx, "whatsapp_lid_dms_migrated", "true")
|
||||
return nil
|
||||
}
|
||||
|
|
@ -2,7 +2,6 @@ package connector
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
|
@ -13,7 +12,6 @@ import (
|
|||
"go.mau.fi/util/exsync"
|
||||
"go.mau.fi/util/jsontime"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"go.mau.fi/whatsmeow/types/events"
|
||||
waLog "go.mau.fi/whatsmeow/util/log"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
|
|
@ -27,7 +25,6 @@ const (
|
|||
LoginStepIDQR = "fi.mau.whatsapp.login.qr"
|
||||
LoginStepIDPhoneNumber = "fi.mau.whatsapp.login.phone"
|
||||
LoginStepIDCode = "fi.mau.whatsapp.login.code"
|
||||
LoginStepIDPasskey = "fi.mau.whatsapp.login.passkey"
|
||||
LoginStepIDComplete = "fi.mau.whatsapp.login.complete"
|
||||
|
||||
LoginFlowIDQR = "qr"
|
||||
|
|
@ -79,8 +76,8 @@ var (
|
|||
}
|
||||
ErrRateLimitedByWhatsApp = bridgev2.RespError{
|
||||
ErrCode: "FI.MAU.WHATSAPP.RATE_LIMITED",
|
||||
Err: "Rate limited by WhatsApp. Try again later.",
|
||||
StatusCode: http.StatusBadRequest,
|
||||
Err: "Rate limited by WhatsApp",
|
||||
StatusCode: http.StatusTooManyRequests,
|
||||
}
|
||||
)
|
||||
|
||||
|
|
@ -96,8 +93,6 @@ func (wa *WhatsAppConnector) CreateLogin(_ context.Context, user *bridgev2.User,
|
|||
|
||||
WaitForQRs: exsync.NewEvent(),
|
||||
LoginComplete: exsync.NewEvent(),
|
||||
PasskeyRequest: exsync.NewEvent(),
|
||||
PasskeyConfirmation: exsync.NewEvent(),
|
||||
Received515: exsync.NewEvent(),
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -119,11 +114,6 @@ type WALogin struct {
|
|||
Received515 *exsync.Event
|
||||
PrevQRIndex atomic.Int32
|
||||
|
||||
PasskeyRequest *exsync.Event
|
||||
PasskeyRequestData *events.PairPasskeyRequest
|
||||
PasskeyConfirmation *exsync.Event
|
||||
PasskeyConfirmationData *events.PairPasskeyConfirmation
|
||||
|
||||
Closed atomic.Bool
|
||||
EventHandlerID uint32
|
||||
}
|
||||
|
|
@ -132,11 +122,9 @@ var (
|
|||
_ bridgev2.LoginProcessDisplayAndWait = (*WALogin)(nil)
|
||||
_ bridgev2.LoginProcessUserInput = (*WALogin)(nil)
|
||||
_ bridgev2.LoginProcessWithOverride = (*WALogin)(nil)
|
||||
_ bridgev2.LoginProcessWebAuthn = (*WALogin)(nil)
|
||||
)
|
||||
|
||||
const LoginConnectWait = 30 * time.Second
|
||||
const LoginPairPhoneWait = 30 * time.Second
|
||||
const LoginConnectWait = 15 * time.Second
|
||||
|
||||
func (wl *WALogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) {
|
||||
wl.Main.firstClientConnectOnce.Do(wl.Main.onFirstClientConnect)
|
||||
|
|
@ -194,21 +182,19 @@ func (wl *WALogin) StartWithOverride(ctx context.Context, old *bridgev2.UserLogi
|
|||
}
|
||||
|
||||
func (wl *WALogin) SubmitUserInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, LoginConnectWait)
|
||||
defer cancel()
|
||||
err := wl.Client.Connect()
|
||||
if err != nil {
|
||||
wl.Log.Err(err).Msg("Failed to connect to WhatsApp for phone code login")
|
||||
return nil, err
|
||||
}
|
||||
connectCtx, cancelConnect := context.WithTimeout(ctx, LoginConnectWait)
|
||||
err = wl.WaitForQRs.Wait(connectCtx)
|
||||
cancelConnect()
|
||||
err = wl.WaitForQRs.Wait(ctx)
|
||||
if err != nil {
|
||||
wl.Log.Warn().Err(err).Msg("Timed out waiting for connection")
|
||||
return nil, fmt.Errorf("failed to wait for connection: %w", err)
|
||||
}
|
||||
pairCtx, cancelPair := context.WithTimeout(ctx, LoginPairPhoneWait)
|
||||
defer cancelPair()
|
||||
pairingCode, err := wl.Client.PairPhone(pairCtx, input["phone_number"], true, whatsmeow.PairClientChrome, "Chrome (Linux)")
|
||||
pairingCode, err := wl.Client.PairPhone(ctx, input["phone_number"], true, whatsmeow.PairClientChrome, "Chrome (Linux)")
|
||||
if err != nil {
|
||||
wl.Log.Err(err).Msg("Failed to request phone code login")
|
||||
if errors.Is(err, whatsmeow.ErrPhoneNumberTooShort) {
|
||||
|
|
@ -276,17 +262,6 @@ func (wl *WALogin) handleEvent(rawEvt any) {
|
|||
case *events.ClientOutdated:
|
||||
wl.Log.Error().Msg("Got client outdated error")
|
||||
wl.LoginError = ErrLoginClientOutdated
|
||||
case *events.PairPasskeyRequest:
|
||||
wl.PasskeyRequestData = evt
|
||||
wl.PasskeyRequest.Set()
|
||||
return
|
||||
case *events.PairPasskeyConfirmation:
|
||||
wl.PasskeyConfirmationData = evt
|
||||
wl.PasskeyConfirmation.Set()
|
||||
return
|
||||
case *events.PairPasskeyError:
|
||||
wl.Log.Err(evt.Error).Msg("Got passkey error")
|
||||
wl.LoginError = evt.Error
|
||||
case *events.PairSuccess:
|
||||
wl.Log.Info().Any("event_data", evt).Msg("Got pair successful event")
|
||||
wl.LoginSuccess = evt
|
||||
|
|
@ -314,14 +289,10 @@ func (wl *WALogin) handleEvent(rawEvt any) {
|
|||
|
||||
func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
|
||||
if wl.PhoneCode {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
err := wl.LoginComplete.Wait(ctx)
|
||||
if err != nil {
|
||||
wl.Cancel()
|
||||
return nil, ctx.Err()
|
||||
case <-wl.PasskeyRequest.GetChan():
|
||||
return wl.makePasskeyStep()
|
||||
case <-wl.LoginComplete.GetChan():
|
||||
// continue
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
prevIndex := int(wl.PrevQRIndex.Load())
|
||||
|
|
@ -348,16 +319,9 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
|
|||
case <-ctx.Done():
|
||||
wl.Cancel()
|
||||
return nil, ctx.Err()
|
||||
case <-wl.PasskeyRequest.GetChan():
|
||||
return wl.makePasskeyStep()
|
||||
case <-wl.LoginComplete.GetChan():
|
||||
// continue
|
||||
}
|
||||
}
|
||||
return wl.onLoginComplete(ctx)
|
||||
}
|
||||
|
||||
func (wl *WALogin) onLoginComplete(ctx context.Context) (*bridgev2.LoginStep, error) {
|
||||
if wl.LoginError != nil {
|
||||
wl.Log.Debug().Err(wl.LoginError).Msg("Login completed with error")
|
||||
wl.Cancel()
|
||||
|
|
@ -408,66 +372,6 @@ func (wl *WALogin) onLoginComplete(ctx context.Context) (*bridgev2.LoginStep, er
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (wl *WALogin) makePasskeyStep() (*bridgev2.LoginStep, error) {
|
||||
pubkeyData, err := json.Marshal(wl.PasskeyRequestData.PublicKey)
|
||||
if err != nil {
|
||||
wl.Log.Err(err).Msg("Failed to marshal public key data")
|
||||
return nil, err
|
||||
}
|
||||
return &bridgev2.LoginStep{
|
||||
Type: bridgev2.LoginStepTypeWebAuthn,
|
||||
StepID: LoginStepIDPasskey,
|
||||
WebAuthnParams: &bridgev2.LoginWebAuthnParams{
|
||||
URL: "https://web.whatsapp.com",
|
||||
PublicKey: pubkeyData,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (wl *WALogin) SubmitWebAuthnResponse(ctx context.Context, rawResp json.RawMessage) (*bridgev2.LoginStep, error) {
|
||||
var resp types.WebAuthnResponse
|
||||
err := json.Unmarshal(rawResp, &resp)
|
||||
if err != nil {
|
||||
wl.Log.Err(err).Msg("Failed to unmarshal WebAuthn response")
|
||||
wl.Cancel()
|
||||
return nil, err
|
||||
}
|
||||
err = wl.Client.SendPasskeyResponse(ctx, &resp)
|
||||
if err != nil {
|
||||
wl.Log.Err(err).Msg("Failed to send WebAuthn response")
|
||||
wl.Cancel()
|
||||
return nil, err
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
wl.Cancel()
|
||||
return nil, ctx.Err()
|
||||
case <-wl.PasskeyConfirmation.GetChan():
|
||||
if !wl.PasskeyConfirmationData.SkipHandoffUX {
|
||||
wl.Cancel()
|
||||
return nil, bridgev2.RespError{
|
||||
ErrCode: "FI.MAU.WHATSAPP.NOT_IMPLEMENTED",
|
||||
Err: "Displaying WebAuthn pairing confirmation codes is not yet implemented",
|
||||
StatusCode: http.StatusBadRequest,
|
||||
}
|
||||
}
|
||||
err = wl.Client.SendPasskeyConfirmation(ctx)
|
||||
if err != nil {
|
||||
wl.Log.Err(err).Msg("Failed to send WebAuthn confirmation")
|
||||
wl.Cancel()
|
||||
return nil, err
|
||||
}
|
||||
case <-wl.LoginComplete.GetChan():
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
wl.Cancel()
|
||||
return nil, ctx.Err()
|
||||
case <-wl.LoginComplete.GetChan():
|
||||
}
|
||||
return wl.onLoginComplete(ctx)
|
||||
}
|
||||
|
||||
func (wl *WALogin) Cancel() {
|
||||
wl.Closed.Store(true)
|
||||
wl.Client.RemoveEventHandler(wl.EventHandlerID)
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import (
|
|||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"go.mau.fi/util/ptr"
|
||||
"go.mau.fi/whatsmeow"
|
||||
waBinary "go.mau.fi/whatsmeow/binary"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
|
|
@ -40,16 +39,14 @@ func (wa *WhatsAppClient) initMC() {
|
|||
}
|
||||
|
||||
type mClient = interface {
|
||||
OnMatrixEvent(any, time.Duration, error)
|
||||
OnWhatsAppEvent(any)
|
||||
OnConnect(version uint32, platform string)
|
||||
}
|
||||
|
||||
type noopMC struct{}
|
||||
|
||||
var noopMCInstance mClient = &noopMC{}
|
||||
|
||||
func (n *noopMC) OnMatrixEvent(any, time.Duration, error) {}
|
||||
func (n *noopMC) OnWhatsAppEvent(any) {}
|
||||
func (n *noopMC) OnConnect(version uint32, platform string) {}
|
||||
|
||||
type mWAClient = interface {
|
||||
MSend(data []byte)
|
||||
|
|
@ -82,7 +79,3 @@ func (wa *WhatsAppClient) MSave(s json.RawMessage) {
|
|||
wa.UserLogin.Log.Err(err).Msg("Failed to save MC data")
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) mcTrack(evt any, start time.Time, err *error) {
|
||||
wa.MC.OnMatrixEvent(evt, time.Since(start), ptr.Val(err))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -70,9 +70,10 @@ func (wa *WhatsAppClient) processFailedMedia(ctx context.Context, portalKey netw
|
|||
func (wa *WhatsAppClient) mediaRequestLoop(ctx context.Context) {
|
||||
log := wa.UserLogin.Log.With().Str("loop", "media requests").Logger()
|
||||
ctx = log.WithContext(ctx)
|
||||
userTz, err := wa.UserLogin.Metadata.(*waid.UserLoginMetadata).LoadTimezone()
|
||||
tzName := wa.UserLogin.Metadata.(*waid.UserLoginMetadata).Timezone
|
||||
userTz, err := time.LoadLocation(tzName)
|
||||
var startIn time.Duration
|
||||
if err == nil && userTz != nil {
|
||||
if tzName != "" && err == nil && userTz != nil {
|
||||
now := time.Now()
|
||||
startAt := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 0, 0, userTz)
|
||||
startAt = startAt.Add(time.Duration(wa.Main.Config.HistorySync.MediaRequests.RequestLocalTime) * time.Minute)
|
||||
|
|
@ -155,8 +156,8 @@ func (wa *WhatsAppClient) sendMediaRequestDirect(ctx context.Context, rawMsgID n
|
|||
return wa.Client.SendMediaRetryReceipt(ctx, &types.MessageInfo{
|
||||
ID: msgID.ID,
|
||||
MessageSource: types.MessageSource{
|
||||
IsFromMe: wa.IsOwnJID(msgID.Sender),
|
||||
IsGroup: msgID.Chat.Server != types.DefaultUserServer && msgID.Chat.Server != types.HiddenUserServer && msgID.Chat.Server != types.BotServer,
|
||||
IsFromMe: msgID.Sender.User == wa.JID.User,
|
||||
IsGroup: msgID.Chat.Server != types.DefaultUserServer && msgID.Chat.Server != types.BotServer,
|
||||
Sender: msgID.Sender,
|
||||
Chat: msgID.Chat,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -37,13 +37,6 @@ func (wa *WhatsAppClient) FillBridgeState(state status.BridgeState) status.Bridg
|
|||
state.Error = WAPhoneOffline
|
||||
state.UserAction = status.UserActionOpenNative
|
||||
}
|
||||
rtu := wa.UserLogin.Metadata.(*waid.UserLoginMetadata).ReachoutTimelockUntil
|
||||
if !rtu.IsZero() {
|
||||
if state.Info == nil {
|
||||
state.Info = make(map[string]any)
|
||||
}
|
||||
state.Info["reachout_timelock_until"] = rtu
|
||||
}
|
||||
return state
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -25,8 +25,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/exmaps"
|
||||
"go.mau.fi/util/exsync"
|
||||
"go.mau.fi/util/ptr"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
|
|
@ -64,13 +62,6 @@ func looksEmaily(str string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
type cacheEntry struct {
|
||||
jid types.JID
|
||||
ts time.Time
|
||||
}
|
||||
|
||||
var isOnWhatsappCache = exsync.NewMap[string, cacheEntry]()
|
||||
|
||||
func (wa *WhatsAppClient) validateIdentifer(ctx context.Context, number string) (types.JID, error) {
|
||||
if strings.HasSuffix(number, "@"+types.BotServer) || strings.HasSuffix(number, "@"+types.HiddenUserServer) {
|
||||
return types.ParseJID(number)
|
||||
|
|
@ -85,8 +76,6 @@ func (wa *WhatsAppClient) validateIdentifer(ctx context.Context, number string)
|
|||
return types.EmptyJID, ErrInputLooksLikeEmail
|
||||
} else if wa.Client == nil || !wa.Client.IsLoggedIn() {
|
||||
return types.EmptyJID, bridgev2.ErrNotLoggedIn
|
||||
} else if entry, ok := isOnWhatsappCache.Get(number); ok && time.Since(entry.ts) < 4*time.Hour {
|
||||
return entry.jid, nil
|
||||
} else if resp, err := wa.Client.IsOnWhatsApp(ctx, []string{number}); err != nil {
|
||||
return types.EmptyJID, fmt.Errorf("failed to check if number is on WhatsApp: %w", err)
|
||||
} else if len(resp) == 0 {
|
||||
|
|
@ -94,7 +83,6 @@ func (wa *WhatsAppClient) validateIdentifer(ctx context.Context, number string)
|
|||
} else if !resp[0].IsIn {
|
||||
return types.EmptyJID, bridgev2.WrapRespErr(fmt.Errorf("the server said +%s is not on WhatsApp", resp[0].JID.User), mautrix.MNotFound)
|
||||
} else {
|
||||
isOnWhatsappCache.Set(number, cacheEntry{resp[0].JID, time.Now()})
|
||||
return resp[0].JID, nil
|
||||
}
|
||||
}
|
||||
|
|
@ -120,24 +108,16 @@ func (wa *WhatsAppConnector) ValidateUserID(id networkid.UserID) bool {
|
|||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) startChatPNToLID(ctx context.Context, jid types.JID) (types.JID, error) {
|
||||
if jid.Server == types.DefaultUserServer {
|
||||
lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, jid)
|
||||
func (wa *WhatsAppClient) startChatLIDToPN(ctx context.Context, jid types.JID) (types.JID, error) {
|
||||
if jid.Server == types.HiddenUserServer {
|
||||
pn, err := wa.GetStore().LIDs.GetPNForLID(ctx, jid)
|
||||
if err != nil {
|
||||
return jid, fmt.Errorf("failed to get lid for phone number: %w", err)
|
||||
} else if lid.IsEmpty() {
|
||||
resp, err := wa.Client.GetUserInfo(ctx, []types.JID{jid})
|
||||
if err != nil {
|
||||
return jid, fmt.Errorf("failed to get user info for phone number: %w", err)
|
||||
} else if info, ok := resp[jid]; !ok {
|
||||
return jid, fmt.Errorf("server didn't return user info for phone number")
|
||||
} else if info.LID.IsEmpty() {
|
||||
return jid, fmt.Errorf("server didn't return lid for phone number")
|
||||
} else {
|
||||
return info.LID, nil
|
||||
return jid, fmt.Errorf("failed to get phone number for lid: %w", err)
|
||||
} else if pn.IsEmpty() {
|
||||
// Don't allow starting chats with LIDs for now
|
||||
return jid, fmt.Errorf("phone number not found")
|
||||
}
|
||||
}
|
||||
return lid, nil
|
||||
return pn, nil
|
||||
}
|
||||
return jid, nil
|
||||
}
|
||||
|
|
@ -156,7 +136,7 @@ func (wa *WhatsAppClient) makeCreateChatResponse(ctx context.Context, jid, origJ
|
|||
|
||||
func (wa *WhatsAppClient) CreateChatWithGhost(ctx context.Context, ghost *bridgev2.Ghost) (*bridgev2.CreateChatResponse, error) {
|
||||
origJID := waid.ParseUserID(ghost.ID)
|
||||
jid, err := wa.startChatPNToLID(ctx, origJID)
|
||||
jid, err := wa.startChatLIDToPN(ctx, origJID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -168,7 +148,7 @@ func (wa *WhatsAppClient) ResolveIdentifier(ctx context.Context, identifier stri
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jid, err := wa.startChatPNToLID(ctx, origJID)
|
||||
jid, err := wa.startChatLIDToPN(ctx, origJID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -208,7 +188,6 @@ func (wa *WhatsAppClient) getContactList(ctx context.Context, filter string, onl
|
|||
return nil, err
|
||||
}
|
||||
resp := make([]*bridgev2.ResolveIdentifierResponse, 0, len(contacts))
|
||||
addedIDs := make(exmaps.Set[types.JID])
|
||||
for jid, contactInfo := range contacts {
|
||||
if onlyContacts && (contactInfo.FirstName == "" && contactInfo.FullName == "") {
|
||||
continue
|
||||
|
|
@ -216,43 +195,31 @@ func (wa *WhatsAppClient) getContactList(ctx context.Context, filter string, onl
|
|||
if !matchesQuery(contactInfo.PushName, filter) && !matchesQuery(contactInfo.FullName, filter) && !matchesQuery(jid.User, filter) {
|
||||
continue
|
||||
}
|
||||
var lid types.JID
|
||||
if jid.Server == types.HiddenUserServer {
|
||||
lid = jid
|
||||
} else if jid.Server == types.DefaultUserServer {
|
||||
lid, err = wa.GetStore().LIDs.GetLIDForPN(ctx, jid)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get lid for phone number %s: %w", jid, err)
|
||||
} else if !lid.IsEmpty() {
|
||||
jid = lid
|
||||
}
|
||||
}
|
||||
if !addedIDs.Add(jid) {
|
||||
continue
|
||||
}
|
||||
var chatResp *bridgev2.CreateChatResponse
|
||||
if !lid.IsEmpty() {
|
||||
chatResp = &bridgev2.CreateChatResponse{PortalKey: wa.makeWAPortalKey(lid)}
|
||||
}
|
||||
ghost, _ := wa.Main.Bridge.GetGhostByID(ctx, waid.MakeUserID(jid))
|
||||
resp = append(resp, &bridgev2.ResolveIdentifierResponse{
|
||||
Ghost: ghost,
|
||||
UserID: waid.MakeUserID(jid),
|
||||
UserInfo: wa.contactToUserInfo(ctx, jid, contactInfo, false),
|
||||
Chat: chatResp,
|
||||
Chat: &bridgev2.CreateChatResponse{PortalKey: wa.makeWAPortalKey(jid)},
|
||||
})
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCreateParams) (*bridgev2.CreateChatResponse, error) {
|
||||
createKey := wa.Client.GenerateMessageID()
|
||||
if params.RoomID != "" {
|
||||
wa.createDedup.Add(createKey)
|
||||
}
|
||||
req := whatsmeow.ReqCreateGroup{
|
||||
Name: ptr.Val(params.Name).Name,
|
||||
Participants: make([]types.JID, len(params.Participants)),
|
||||
CreateKey: createKey,
|
||||
}
|
||||
for i, participant := range params.Participants {
|
||||
jid := waid.ParseUserID(participant)
|
||||
jid, err := wa.startChatPNToLID(ctx, jid)
|
||||
// Normalize to PN if it's a LID
|
||||
jid, err := wa.startChatLIDToPN(ctx, jid)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to normalize participant %s: %w", participant, err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,8 +29,7 @@ var ResyncLoopInterval = 4 * time.Hour
|
|||
var ResyncJitterSeconds = 3600
|
||||
|
||||
func (wa *WhatsAppClient) EnqueueGhostResync(ghost *bridgev2.Ghost) {
|
||||
lastSync := ghost.Metadata.(*waid.GhostMetadata).LastSync.Time
|
||||
if lastSync.Add(ResyncMinInterval).After(time.Now()) {
|
||||
if ghost.Metadata.(*waid.GhostMetadata).LastSync.Add(ResyncMinInterval).After(time.Now()) {
|
||||
return
|
||||
}
|
||||
wa.resyncQueueLock.Lock()
|
||||
|
|
@ -44,7 +43,6 @@ func (wa *WhatsAppClient) EnqueueGhostResync(ghost *bridgev2.Ghost) {
|
|||
wa.UserLogin.Log.Debug().
|
||||
Stringer("jid", jid).
|
||||
Str("next_resync_in", nextResyncIn).
|
||||
Time("last_ghost_resync", lastSync).
|
||||
Msg("Enqueued resync for ghost")
|
||||
}
|
||||
wa.resyncQueueLock.Unlock()
|
||||
|
|
@ -52,8 +50,7 @@ func (wa *WhatsAppClient) EnqueueGhostResync(ghost *bridgev2.Ghost) {
|
|||
|
||||
func (wa *WhatsAppClient) EnqueuePortalResync(portal *bridgev2.Portal, allowDM bool) {
|
||||
jid, _ := waid.ParsePortalID(portal.ID)
|
||||
lastSync := portal.Metadata.(*waid.PortalMetadata).LastSync.Time
|
||||
if lastSync.Add(ResyncMinInterval).After(time.Now()) {
|
||||
if portal.Metadata.(*waid.PortalMetadata).LastSync.Add(ResyncMinInterval).After(time.Now()) {
|
||||
return
|
||||
} else if !allowDM && jid.Server != types.GroupServer {
|
||||
return
|
||||
|
|
@ -64,7 +61,6 @@ func (wa *WhatsAppClient) EnqueuePortalResync(portal *bridgev2.Portal, allowDM b
|
|||
wa.UserLogin.Log.Debug().
|
||||
Stringer("jid", jid).
|
||||
Stringer("next_resync_in", time.Until(wa.nextResync)).
|
||||
Time("last_portal_resync", lastSync).
|
||||
Msg("Enqueued resync for portal")
|
||||
}
|
||||
wa.resyncQueueLock.Unlock()
|
||||
|
|
@ -361,7 +357,7 @@ func (wa *WhatsAppClient) fetchGhostAvatar(ctx context.Context, ghost *bridgev2.
|
|||
wrappedAvatar = &bridgev2.Avatar{
|
||||
ID: networkid.AvatarID(avatar.ID),
|
||||
Get: func(ctx context.Context) ([]byte, error) {
|
||||
return wa.Client.DownloadMediaWithOnlyPath(ctx, avatar.DirectPath)
|
||||
return wa.Client.DownloadMediaWithPath(ctx, avatar.DirectPath, nil, nil, nil, 0, "", "")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -96,12 +96,12 @@ func (mq *MessageQuery) GetBetween(ctx context.Context, loginID networkid.UserLo
|
|||
args := []any{mq.BridgeID, loginID, chatJID}
|
||||
argNum := 4
|
||||
if startTime != nil {
|
||||
whereClauses += fmt.Sprintf(" AND timestamp > $%d", argNum)
|
||||
whereClauses += fmt.Sprintf(" AND timestamp >= $%d", argNum)
|
||||
args = append(args, startTime.Unix())
|
||||
argNum++
|
||||
}
|
||||
if endTime != nil {
|
||||
whereClauses += fmt.Sprintf(" AND timestamp < $%d", argNum)
|
||||
whereClauses += fmt.Sprintf(" AND timestamp <= $%d", argNum)
|
||||
args = append(args, endTime.Unix())
|
||||
}
|
||||
|
||||
|
|
@ -116,12 +116,9 @@ func (mq *MessageQuery) GetBetween(ctx context.Context, loginID networkid.UserLo
|
|||
AsList()
|
||||
}
|
||||
|
||||
func (mq *MessageQuery) DeleteBetween(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID, before, after uint64) (int64, error) {
|
||||
res, err := mq.Exec(ctx, deleteHistorySyncMessagesBetweenQuery, mq.BridgeID, loginID, chatJID, before, after)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.RowsAffected()
|
||||
func (mq *MessageQuery) DeleteBetween(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID, before, after uint64) error {
|
||||
_, err := mq.Exec(ctx, deleteHistorySyncMessagesBetweenQuery, mq.BridgeID, loginID, chatJID, before, after)
|
||||
return err
|
||||
}
|
||||
|
||||
func (mq *MessageQuery) DeleteAll(ctx context.Context, loginID networkid.UserLoginID) error {
|
||||
|
|
@ -129,12 +126,9 @@ func (mq *MessageQuery) DeleteAll(ctx context.Context, loginID networkid.UserLog
|
|||
return err
|
||||
}
|
||||
|
||||
func (mq *MessageQuery) DeleteAllInChat(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID) (int64, error) {
|
||||
res, err := mq.Exec(ctx, deleteHistorySyncMessagesForPortalQuery, mq.BridgeID, loginID, chatJID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.RowsAffected()
|
||||
func (mq *MessageQuery) DeleteAllInChat(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID) error {
|
||||
_, err := mq.Exec(ctx, deleteHistorySyncMessagesForPortalQuery, mq.BridgeID, loginID, chatJID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (mq *MessageQuery) ConversationHasMessages(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID) (exists bool, err error) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
-- v0 -> v10 (compatible with v3+): Latest revision
|
||||
-- v0 -> v9 (compatible with v3+): Latest revision
|
||||
|
||||
CREATE TABLE whatsapp_poll_option_id (
|
||||
bridge_id TEXT NOT NULL,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
-- v8 (compatible with v3+): Mark LID DMs for deletion
|
||||
INSERT INTO kv_store (bridge_id, key, value) VALUES ('', 'whatsapp_lid_dms_deleted', 'false');
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
-- v9 (compatible with v3+): Mark LID DMs for deletion (again)
|
||||
DELETE FROM kv_store WHERE bridge_id='' AND key='whatsapp_lid_dms_deleted';
|
||||
INSERT INTO kv_store (bridge_id, key, value) VALUES ('', 'whatsapp_lid_dms_deleted', 'false');
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
-- v10 (compatible with v3+): Move history sync conversations to LIDs
|
||||
|
||||
-- Delete history sync conversations where a @lid conversation already exists
|
||||
DELETE FROM whatsapp_history_sync_conversation
|
||||
WHERE chat_jid LIKE '%@lid' AND EXISTS (
|
||||
SELECT 1
|
||||
FROM whatsapp_history_sync_conversation pnconv
|
||||
WHERE pnconv.chat_jid=(
|
||||
SELECT pn || '@s.whatsapp.net'
|
||||
FROM whatsmeow_lid_map
|
||||
WHERE lid=replace(whatsapp_history_sync_conversation.chat_jid, '@lid', '')
|
||||
)
|
||||
);
|
||||
|
||||
-- Update all phone number conversations to lids if the lid is known
|
||||
UPDATE whatsapp_history_sync_conversation
|
||||
SET chat_jid=(SELECT lid || '@lid' FROM whatsmeow_lid_map WHERE pn=replace(chat_jid, '@s.whatsapp.net', ''))
|
||||
WHERE chat_jid LIKE '%@s.whatsapp.net'
|
||||
AND EXISTS (SELECT 1 FROM whatsmeow_lid_map WHERE pn=replace(chat_jid, '@s.whatsapp.net', ''));
|
||||
|
||||
-- Delete blank phone number portals
|
||||
DELETE FROM portal WHERE id LIKE '%@s.whatsapp.net' AND (mxid IS NULL OR mxid='') AND room_type='';
|
||||
|
|
@ -6,9 +6,11 @@ import (
|
|||
"go.mau.fi/util/dbutil"
|
||||
)
|
||||
|
||||
var Table dbutil.UpgradeTable
|
||||
|
||||
//go:embed *.sql
|
||||
var rawUpgrades embed.FS
|
||||
|
||||
var Table = dbutil.BuildUpgradeTable().
|
||||
WithFS(rawUpgrades).
|
||||
Finish()
|
||||
func init() {
|
||||
Table.RegisterFS(rawUpgrades)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -67,11 +67,10 @@ func getMessageType(waMsg *waE2E.Message) string {
|
|||
return "encrypted comment"
|
||||
case waMsg.CommentMessage != nil:
|
||||
return "comment"
|
||||
case waMsg.PollCreationMessage != nil || waMsg.PollCreationMessageV2 != nil || waMsg.PollCreationMessageV3 != nil ||
|
||||
waMsg.PollCreationMessageV5 != nil || waMsg.PollCreationMessageV6 != nil:
|
||||
case waMsg.PollCreationMessage != nil || waMsg.PollCreationMessageV2 != nil || waMsg.PollCreationMessageV3 != nil:
|
||||
return "poll create"
|
||||
case waMsg.PollCreationMessageV4 != nil:
|
||||
return "poll create (v4)"
|
||||
case waMsg.PollCreationMessageV4 != nil || waMsg.PollCreationMessageV5 != nil:
|
||||
return "poll create (vNext)"
|
||||
case waMsg.PollUpdateMessage != nil:
|
||||
return "poll update"
|
||||
case waMsg.ProtocolMessage != nil:
|
||||
|
|
@ -131,6 +130,8 @@ func getMessageType(waMsg *waE2E.Message) string {
|
|||
return "secret encrypted"
|
||||
case waMsg.PollResultSnapshotMessage != nil:
|
||||
return "poll result snapshot"
|
||||
case waMsg.MessageHistoryBundle != nil:
|
||||
return "message history bundle"
|
||||
case waMsg.MessageHistoryNotice != nil:
|
||||
return "message history notice"
|
||||
case waMsg.RequestPhoneNumberMessage != nil:
|
||||
|
|
@ -153,7 +154,7 @@ func getMessageType(waMsg *waE2E.Message) string {
|
|||
return "chat"
|
||||
case waMsg.PlaceholderMessage != nil:
|
||||
return "placeholder"
|
||||
case waMsg.SenderKeyDistributionMessage != nil, waMsg.StickerSyncRmrMessage != nil, waMsg.MessageHistoryBundle != nil:
|
||||
case waMsg.SenderKeyDistributionMessage != nil, waMsg.StickerSyncRmrMessage != nil:
|
||||
return "ignore"
|
||||
default:
|
||||
return "unknown"
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ package msgconv
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
|
@ -143,14 +142,14 @@ func (mc *MessageConverter) ToWhatsApp(
|
|||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to parse message ID: %w", err)
|
||||
}
|
||||
rootMsgInfo := MessageIDToInfo(ctx, client, parsedID)
|
||||
rootMsgInfo := MessageIDToInfo(client, parsedID)
|
||||
message, err = client.EncryptComment(ctx, rootMsgInfo, message)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to encrypt comment: %w", err)
|
||||
}
|
||||
lid := parsedID.Sender
|
||||
if lid.Server == types.DefaultUserServer {
|
||||
lid, err = client.Store.LIDs.GetLIDForPN(ctx, lid)
|
||||
lid, err = client.Store.LIDs.GetLIDForPN(ctx, parsedID.Sender)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to get LID for PN: %w", err)
|
||||
}
|
||||
|
|
@ -202,7 +201,6 @@ func (mc *MessageConverter) constructMediaMessage(
|
|||
FileSHA256: uploaded.FileSHA256,
|
||||
FileLength: proto.Uint64(uploaded.FileLength),
|
||||
URL: proto.String(uploaded.URL),
|
||||
IsLottie: proto.Bool(mime == "application/was"),
|
||||
},
|
||||
}
|
||||
case event.MsgAudio:
|
||||
|
|
@ -484,17 +482,6 @@ func (mc *MessageConverter) convertToWebP(img []byte) ([]byte, int, error) {
|
|||
return webpBuffer.Bytes(), size, nil
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) getOriginalBridgedSticker(ctx context.Context, info *event.BridgedSticker) (*types.StickerPackItem, error) {
|
||||
if info == nil || info.Network != StickerSourceID || !strings.HasPrefix(info.PackURL, StickerPackURLPrefix) || info.ID == "" {
|
||||
return nil, nil
|
||||
}
|
||||
fileHash, err := base64.StdEncoding.DecodeString(info.ID)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
return mc.GetCachedSticker(ctx, getClient(ctx), strings.TrimPrefix(info.PackURL, StickerPackURLPrefix), fileHash)
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) reuploadFileToWhatsApp(
|
||||
ctx context.Context, content *event.MessageEventContent,
|
||||
) (*whatsmeow.UploadResponse, []byte, string, error) {
|
||||
|
|
@ -503,25 +490,7 @@ func (mc *MessageConverter) reuploadFileToWhatsApp(
|
|||
if content.FileName != "" {
|
||||
fileName = content.FileName
|
||||
}
|
||||
var data []byte
|
||||
var err error
|
||||
var sticker *types.StickerPackItem
|
||||
if sticker, err = mc.getOriginalBridgedSticker(ctx, content.Info.BridgedSticker); err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).
|
||||
Msg("Failed to get original bridged sticker, falling back to downloading from URL")
|
||||
data, err = mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
|
||||
} else if sticker != nil {
|
||||
if sticker.MimeType == "application/was" {
|
||||
data, err = getClient(ctx).Download(ctx, sticker)
|
||||
mime = sticker.MimeType
|
||||
} else {
|
||||
data, err = mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
|
||||
}
|
||||
content.Info.Width = sticker.Width
|
||||
content.Info.Height = sticker.Height
|
||||
} else {
|
||||
data, err = mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
|
||||
}
|
||||
data, err := mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
|
||||
if err != nil {
|
||||
return nil, nil, "", fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err)
|
||||
}
|
||||
|
|
@ -539,14 +508,7 @@ func (mc *MessageConverter) reuploadFileToWhatsApp(
|
|||
case event.MessageType(event.EventSticker.Type):
|
||||
isSticker = true
|
||||
mediaType = whatsmeow.MediaImage
|
||||
if mime == "video/lottie+json" {
|
||||
// This likely won't work
|
||||
data, err = PackAnimatedSticker(data)
|
||||
if err != nil {
|
||||
return nil, nil, mime, fmt.Errorf("%w (packing animated sticker): %w", bridgev2.ErrMediaConvertFailed, err)
|
||||
}
|
||||
mime = "application/was"
|
||||
} else if (mime != "image/webp" || content.Info.Width != content.Info.Height) && mime != "application/was" {
|
||||
if mime != "image/webp" || content.Info.Width != content.Info.Height {
|
||||
var size int
|
||||
data, size, err = mc.convertToWebP(data)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -175,12 +175,6 @@ func (mc *MessageConverter) ToMatrix(
|
|||
part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV2)
|
||||
case waMsg.PollCreationMessageV3 != nil:
|
||||
part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV3)
|
||||
//case waMsg.PollCreationMessageV4 != nil:
|
||||
// part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV4)
|
||||
case waMsg.PollCreationMessageV5 != nil:
|
||||
part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV5)
|
||||
case waMsg.PollCreationMessageV6 != nil:
|
||||
part, contextInfo = mc.convertPollCreationMessage(ctx, waMsg.PollCreationMessageV6)
|
||||
case waMsg.PollUpdateMessage != nil:
|
||||
part, contextInfo = mc.convertPollUpdateMessage(ctx, info, waMsg.PollUpdateMessage)
|
||||
case waMsg.EventMessage != nil:
|
||||
|
|
@ -221,10 +215,12 @@ func (mc *MessageConverter) ToMatrix(
|
|||
part, contextInfo = mc.convertPlaceholderMessage(ctx, waMsg)
|
||||
case waMsg.GroupInviteMessage != nil:
|
||||
part, contextInfo = mc.convertGroupInviteMessage(ctx, info, waMsg.GroupInviteMessage)
|
||||
case waMsg.MessageHistoryNotice != nil:
|
||||
part, contextInfo = mc.convertMessageHistoryNotice(ctx, info, waMsg.MessageHistoryNotice)
|
||||
case waMsg.ProtocolMessage != nil && waMsg.ProtocolMessage.GetType() == waE2E.ProtocolMessage_EPHEMERAL_SETTING:
|
||||
part, contextInfo = mc.convertEphemeralSettingMessage(ctx, waMsg.ProtocolMessage, info.Timestamp, isBackfill)
|
||||
case waMsg.MessageHistoryBundle != nil:
|
||||
part, contextInfo = mc.convertMessageHistoryShare(ctx, info, waMsg.MessageHistoryBundle.GetMessageHistoryMetadata(), waMsg.MessageHistoryBundle.GetContextInfo())
|
||||
case waMsg.MessageHistoryNotice != nil:
|
||||
part, contextInfo = mc.convertMessageHistoryShare(ctx, info, waMsg.MessageHistoryNotice.GetMessageHistoryMetadata(), waMsg.MessageHistoryNotice.GetContextInfo())
|
||||
case waMsg.EncCommentMessage != nil:
|
||||
part = failedCommentPart
|
||||
default:
|
||||
|
|
@ -271,26 +267,30 @@ func (mc *MessageConverter) ToMatrix(
|
|||
if chat.IsEmpty() {
|
||||
chat, _ = waid.ParsePortalID(portal.ID)
|
||||
}
|
||||
// We reroute all DMs to the phone number JID, so reroute reply participants too
|
||||
pcp = rerouteMessageKey(ctx, chat, pcp, getPortal(ctx).Metadata.(*waid.PortalMetadata).AddressingMode == types.AddressingModeLID)
|
||||
if store := getClient(ctx).Store; store != nil && chat.Server == types.DefaultUserServer && pcp.Server == types.HiddenUserServer {
|
||||
pcpPN, _ := store.LIDs.GetPNForLID(ctx, pcp)
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Stringer("orig_participant", pcp).
|
||||
Stringer("rerouted_participant", pcpPN).
|
||||
Msg("Rerouting reply target (PN recipient in LID DM)")
|
||||
if !pcpPN.IsEmpty() {
|
||||
pcp = pcpPN
|
||||
}
|
||||
} else if store != nil && chat.Server == types.GroupServer && pcp.Server == types.DefaultUserServer && getPortal(ctx).Metadata.(*waid.PortalMetadata).AddressingMode == types.AddressingModeLID {
|
||||
pcpLID, _ := store.LIDs.GetLIDForPN(ctx, pcp)
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Stringer("orig_participant", pcp).
|
||||
Stringer("rerouted_participant", pcpLID).
|
||||
Msg("Rerouting reply target (PN recipient in LID group)")
|
||||
if !pcpLID.IsEmpty() {
|
||||
pcp = pcpLID
|
||||
}
|
||||
}
|
||||
cm.ReplyTo = &networkid.MessageOptionalPartID{
|
||||
MessageID: waid.MakeMessageID(chat, pcp, contextInfo.GetStanzaID()),
|
||||
}
|
||||
var pn, lid types.JID
|
||||
if pcp.Server == types.DefaultUserServer {
|
||||
pn = pcp
|
||||
lid, _ = client.Store.LIDs.GetLIDForPN(ctx, pcp)
|
||||
} else if pcp.Server == types.HiddenUserServer {
|
||||
lid = pcp
|
||||
pn, _ = client.Store.LIDs.GetPNForLID(ctx, pcp)
|
||||
} else if pcp.Server == types.BotServer {
|
||||
lid = pcp
|
||||
}
|
||||
if !pn.IsEmpty() {
|
||||
cm.ReplyToLogin = waid.MakeUserLoginID(pn)
|
||||
}
|
||||
if !lid.IsEmpty() {
|
||||
cm.ReplyToUser = waid.MakeUserID(lid)
|
||||
}
|
||||
// TODO set reply to room
|
||||
}
|
||||
if contextInfo.GetIsForwarded() {
|
||||
hasCaption := part.Content.FileName != "" && part.Content.FileName != part.Content.Body
|
||||
|
|
|
|||
|
|
@ -115,7 +115,7 @@ func (mc *MessageConverter) PollVoteToWhatsApp(
|
|||
zerolog.Ctx(ctx).Err(err).Msg("Failed to parse message ID")
|
||||
return nil, fmt.Errorf("failed to parse message ID")
|
||||
}
|
||||
pollMsgInfo := MessageIDToInfo(ctx, client, parsedMsgID)
|
||||
pollMsgInfo := MessageIDToInfo(client, parsedMsgID)
|
||||
pollMsgInfo.Type = "poll"
|
||||
optionHashes := make([][]byte, 0, len(content.Response.Answers))
|
||||
if pollMsg.Metadata.(*waid.MessageMetadata).IsMatrixPoll {
|
||||
|
|
@ -146,23 +146,13 @@ func (mc *MessageConverter) PollVoteToWhatsApp(
|
|||
return &waE2E.Message{PollUpdateMessage: pollUpdate}, err
|
||||
}
|
||||
|
||||
func MessageIDToInfo(ctx context.Context, client *whatsmeow.Client, parsedMsgID *waid.ParsedMessageID) *types.MessageInfo {
|
||||
chat := parsedMsgID.Chat
|
||||
sender := parsedMsgID.Sender
|
||||
if chat.Server == types.DefaultUserServer {
|
||||
chatLID, _ := client.Store.LIDs.GetLIDForPN(ctx, chat)
|
||||
senderLID, _ := client.Store.LIDs.GetLIDForPN(ctx, sender)
|
||||
if !chatLID.IsEmpty() && !senderLID.IsEmpty() {
|
||||
chat = chatLID
|
||||
sender = senderLID
|
||||
}
|
||||
}
|
||||
func MessageIDToInfo(client *whatsmeow.Client, parsedMsgID *waid.ParsedMessageID) *types.MessageInfo {
|
||||
return &types.MessageInfo{
|
||||
MessageSource: types.MessageSource{
|
||||
Chat: chat,
|
||||
Sender: sender,
|
||||
IsFromMe: sender.User == client.Store.GetLID().User || sender.User == client.Store.GetJID().User,
|
||||
IsGroup: chat.Server == types.GroupServer,
|
||||
Chat: parsedMsgID.Chat,
|
||||
Sender: parsedMsgID.Sender,
|
||||
IsFromMe: parsedMsgID.Sender.User == client.Store.GetLID().User || parsedMsgID.Sender.User == client.Store.GetJID().User,
|
||||
IsGroup: parsedMsgID.Chat.Server == types.GroupServer,
|
||||
},
|
||||
ID: parsedMsgID.ID,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,9 +17,6 @@
|
|||
package msgconv
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/format"
|
||||
|
||||
|
|
@ -46,16 +43,12 @@ type MessageConverter struct {
|
|||
DisableViewOnce bool
|
||||
DirectMedia bool
|
||||
OldMediaSuffix string
|
||||
|
||||
stickerPackCache map[string]*types.StickerPack
|
||||
stickerPackCacheLock sync.Mutex
|
||||
}
|
||||
|
||||
func New(br *bridgev2.Bridge) *MessageConverter {
|
||||
mc := &MessageConverter{
|
||||
Bridge: br,
|
||||
MaxFileSize: 50 * 1024 * 1024,
|
||||
stickerPackCache: make(map[string]*types.StickerPack),
|
||||
}
|
||||
mc.HTMLParser = &format.HTMLParser{
|
||||
PillConverter: mc.convertPill,
|
||||
|
|
|
|||
|
|
@ -20,7 +20,6 @@ import (
|
|||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"html"
|
||||
"image"
|
||||
"math"
|
||||
"net/http"
|
||||
|
|
@ -52,7 +51,7 @@ func (mc *MessageConverter) convertLocationMessage(ctx context.Context, msg *waE
|
|||
MsgType: event.MsgLocation,
|
||||
Body: fmt.Sprintf("Location: %s\n%s\n%s", name, msg.GetAddress(), url),
|
||||
Format: event.FormatHTML,
|
||||
FormattedBody: fmt.Sprintf(`Location: <a href="%s">%s</a><br>%s`, html.EscapeString(url), html.EscapeString(name), html.EscapeString(msg.GetAddress())),
|
||||
FormattedBody: fmt.Sprintf("Location: <a href='%s'>%s</a><br>%s", url, name, msg.GetAddress()),
|
||||
GeoURI: fmt.Sprintf("geo:%.5f,%.5f", msg.GetDegreesLatitude(), msg.GetDegreesLongitude()),
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
package msgconv
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
|
@ -24,19 +26,21 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/exfmt"
|
||||
"go.mau.fi/util/exmime"
|
||||
"go.mau.fi/util/exslices"
|
||||
"go.mau.fi/util/lottie"
|
||||
"go.mau.fi/util/random"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/proto/waE2E"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/database"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
"go.mau.fi/mautrix-whatsapp/pkg/waid"
|
||||
)
|
||||
|
|
@ -83,15 +87,15 @@ func (mc *MessageConverter) convertMediaMessage(
|
|||
MimeType: msg.GetMimetype(),
|
||||
}
|
||||
if mc.DirectMedia {
|
||||
preparedMedia.FillFileName()
|
||||
if preparedMedia.Info.MimeType == "application/was" {
|
||||
preparedMedia.Info.MimeType = "video/lottie+json"
|
||||
preparedMedia.FileName = "sticker.json"
|
||||
}
|
||||
preparedMedia.FillFileName()
|
||||
var err error
|
||||
portal := getPortal(ctx)
|
||||
idOverride := getEditTargetID(ctx)
|
||||
preparedMedia.URL, err = portal.Bridge.Matrix.GenerateContentURI(ctx, waid.MakeMediaID(messageInfo, idOverride, portal.Receiver, getMediaIDVersion(msg)))
|
||||
preparedMedia.URL, err = portal.Bridge.Matrix.GenerateContentURI(ctx, waid.MakeMediaID(messageInfo, idOverride, portal.Receiver))
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to generate content URI: %w", err))
|
||||
}
|
||||
|
|
@ -120,20 +124,13 @@ func (mc *MessageConverter) convertMediaMessage(
|
|||
return
|
||||
}
|
||||
|
||||
func getMediaIDVersion(msg MediaMessage) []byte {
|
||||
if encSHA256 := msg.GetFileEncSHA256(); len(encSHA256) > 0 {
|
||||
return encSHA256
|
||||
}
|
||||
return msg.GetFileSHA256()
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) convertAlbumMessage(ctx context.Context, msg *waE2E.AlbumMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
|
||||
parts := make([]string, 0, 2)
|
||||
if msg.GetExpectedImageCount() > 0 {
|
||||
parts = append(parts, exfmt.Pluralizable("image")(int(msg.GetExpectedImageCount())))
|
||||
parts = append(parts, fmt.Sprintf("%d images", msg.GetExpectedImageCount()))
|
||||
}
|
||||
if msg.GetExpectedVideoCount() > 0 {
|
||||
parts = append(parts, exfmt.Pluralizable("video")(int(msg.GetExpectedVideoCount())))
|
||||
parts = append(parts, fmt.Sprintf("%d videos", msg.GetExpectedVideoCount()))
|
||||
}
|
||||
var partDesc string
|
||||
if len(parts) > 0 {
|
||||
|
|
@ -201,9 +198,7 @@ type PreparedMedia struct {
|
|||
}
|
||||
|
||||
func (pm *PreparedMedia) FillFileName() *PreparedMedia {
|
||||
if pm.Type == event.EventSticker {
|
||||
pm.FileName = ""
|
||||
} else if pm.FileName == "" {
|
||||
if pm.FileName == "" {
|
||||
pm.FileName = strings.TrimPrefix(string(pm.MsgType), "m.") + exmime.ExtensionFromMimetype(pm.Info.MimeType)
|
||||
}
|
||||
return pm
|
||||
|
|
@ -244,19 +239,6 @@ type MediaMessageWithDuration interface {
|
|||
|
||||
const WhatsAppStickerSize = 190
|
||||
|
||||
func fixStickerDimensions(info *event.FileInfo) {
|
||||
if info.Width == info.Height {
|
||||
info.Width = WhatsAppStickerSize
|
||||
info.Height = WhatsAppStickerSize
|
||||
} else if info.Width > info.Height {
|
||||
info.Height /= info.Width / WhatsAppStickerSize
|
||||
info.Width = WhatsAppStickerSize
|
||||
} else {
|
||||
info.Width /= info.Height / WhatsAppStickerSize
|
||||
info.Height = WhatsAppStickerSize
|
||||
}
|
||||
}
|
||||
|
||||
func prepareMediaMessage(rawMsg MediaMessage) *PreparedMedia {
|
||||
extraInfo := map[string]any{}
|
||||
data := &PreparedMedia{
|
||||
|
|
@ -305,7 +287,19 @@ func prepareMediaMessage(rawMsg MediaMessage) *PreparedMedia {
|
|||
case *waE2E.StickerMessage:
|
||||
data.Type = event.EventSticker
|
||||
data.FileName = "sticker" + exmime.ExtensionFromMimetype(msg.GetMimetype())
|
||||
fixStickerDimensions(data.Info)
|
||||
if msg.GetMimetype() == "application/was" && data.FileName == "sticker" {
|
||||
data.FileName = "sticker.json"
|
||||
}
|
||||
if data.Info.Width == data.Info.Height {
|
||||
data.Info.Width = WhatsAppStickerSize
|
||||
data.Info.Height = WhatsAppStickerSize
|
||||
} else if data.Info.Width > data.Info.Height {
|
||||
data.Info.Height /= data.Info.Width / WhatsAppStickerSize
|
||||
data.Info.Width = WhatsAppStickerSize
|
||||
} else {
|
||||
data.Info.Width /= data.Info.Height / WhatsAppStickerSize
|
||||
data.Info.Height = WhatsAppStickerSize
|
||||
}
|
||||
case *waE2E.VideoMessage:
|
||||
data.MsgType = event.MsgVideo
|
||||
pairedMediaType := msg.GetContextInfo().GetPairedMediaType()
|
||||
|
|
@ -365,17 +359,14 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
|
|||
) error {
|
||||
client := getClient(ctx)
|
||||
intent := getIntent(ctx)
|
||||
var roomID id.RoomID
|
||||
if portal := getPortal(ctx); portal != nil {
|
||||
roomID = portal.MXID
|
||||
}
|
||||
portal := getPortal(ctx)
|
||||
var thumbnailData []byte
|
||||
var thumbnailInfo *event.FileInfo
|
||||
if part.Info.Size > uploadFileThreshold {
|
||||
var err error
|
||||
part.URL, part.File, err = intent.UploadMediaStream(ctx, roomID, -1, true, func(file io.Writer) (*bridgev2.FileStreamResult, error) {
|
||||
part.URL, part.File, err = intent.UploadMediaStream(ctx, portal.MXID, -1, true, func(file io.Writer) (*bridgev2.FileStreamResult, error) {
|
||||
err := client.DownloadToFile(ctx, message, file.(*os.File))
|
||||
if errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
|
||||
if errors.Is(err, whatsmeow.ErrFileLengthMismatch) || errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Mismatching media checksums in message. Ignoring because WhatsApp seems to ignore them too")
|
||||
} else if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err)
|
||||
|
|
@ -396,7 +387,7 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
|
|||
}
|
||||
} else {
|
||||
data, err := client.Download(ctx, message)
|
||||
if errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
|
||||
if errors.Is(err, whatsmeow.ErrFileLengthMismatch) || errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Mismatching media checksums in message. Ignoring because WhatsApp seems to ignore them too")
|
||||
} else if err != nil {
|
||||
return fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err)
|
||||
|
|
@ -406,14 +397,12 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if part.Type == event.EventSticker && part.Info.MimeType == "image/webp" {
|
||||
mc.fillWebPStickerInfo(ctx, part, data)
|
||||
}
|
||||
if part.Info.MimeType == "" {
|
||||
part.Info.MimeType = http.DetectContentType(data)
|
||||
}
|
||||
part.FillFileName()
|
||||
part.URL, part.File, err = intent.UploadMedia(ctx, roomID, data, part.FileName, part.Info.MimeType)
|
||||
part.URL, part.File, err = intent.UploadMedia(ctx, portal.MXID, data, part.FileName, part.Info.MimeType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", bridgev2.ErrMediaReuploadFailed, err)
|
||||
}
|
||||
|
|
@ -422,7 +411,7 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
|
|||
var err error
|
||||
part.Info.ThumbnailURL, part.Info.ThumbnailFile, err = intent.UploadMedia(
|
||||
ctx,
|
||||
roomID,
|
||||
portal.MXID,
|
||||
thumbnailData,
|
||||
"thumbnail"+exmime.ExtensionFromMimetype(thumbnailInfo.MimeType),
|
||||
thumbnailInfo.MimeType,
|
||||
|
|
@ -436,6 +425,68 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
|
|||
return nil
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) extractAnimatedSticker(fileInfo *PreparedMedia, data []byte) ([]byte, error) {
|
||||
data, err := ExtractAnimatedSticker(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
fileInfo.Info.MimeType = "video/lottie+json"
|
||||
fileInfo.FileName = "sticker.json"
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) convertAnimatedSticker(ctx context.Context, fileInfo *PreparedMedia, data []byte) ([]byte, []byte, *event.FileInfo, error) {
|
||||
data, err := mc.extractAnimatedSticker(fileInfo, data)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
c := mc.AnimatedStickerConfig
|
||||
if c.Target == "disable" {
|
||||
return data, nil, nil, nil
|
||||
} else if !lottie.Supported() {
|
||||
zerolog.Ctx(ctx).Warn().Msg("Animated sticker conversion is enabled, but lottieconverter is not installed")
|
||||
return data, nil, nil, nil
|
||||
}
|
||||
input := bytes.NewReader(data)
|
||||
fileInfo.Info.MimeType = "image/" + c.Target
|
||||
fileInfo.FileName = "sticker." + c.Target
|
||||
switch c.Target {
|
||||
case "png":
|
||||
var output bytes.Buffer
|
||||
err = lottie.Convert(ctx, input, "", &output, c.Target, c.Args.Width, c.Args.Height, "1")
|
||||
return output.Bytes(), nil, nil, err
|
||||
case "gif":
|
||||
var output bytes.Buffer
|
||||
err = lottie.Convert(ctx, input, "", &output, c.Target, c.Args.Width, c.Args.Height, strconv.Itoa(c.Args.FPS))
|
||||
return output.Bytes(), nil, nil, err
|
||||
case "webm", "webp":
|
||||
tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("mautrix-whatsapp-lottieconverter-%s.%s", random.String(10), c.Target))
|
||||
defer func() {
|
||||
_ = os.Remove(tmpFile)
|
||||
}()
|
||||
thumbnailData, err := lottie.FFmpegConvert(ctx, input, tmpFile, c.Args.Width, c.Args.Height, c.Args.FPS)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
data, err = os.ReadFile(tmpFile)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("failed to read converted file: %w", err)
|
||||
}
|
||||
var thumbnailInfo *event.FileInfo
|
||||
if thumbnailData != nil {
|
||||
thumbnailInfo = &event.FileInfo{
|
||||
MimeType: "image/png",
|
||||
Width: c.Args.Width,
|
||||
Height: c.Args.Height,
|
||||
Size: len(thumbnailData),
|
||||
}
|
||||
}
|
||||
return data, thumbnailData, thumbnailInfo, nil
|
||||
default:
|
||||
return nil, nil, nil, fmt.Errorf("unsupported target format %s", c.Target)
|
||||
}
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) makeMediaFailure(ctx context.Context, mediaInfo *PreparedMedia, keys *FailedMediaKeys, err error) *bridgev2.ConvertedMessagePart {
|
||||
logLevel := zerolog.ErrorLevel
|
||||
var extra map[string]any
|
||||
|
|
@ -480,3 +531,28 @@ func (mc *MessageConverter) makeMediaFailure(ctx context.Context, mediaInfo *Pre
|
|||
}
|
||||
return part
|
||||
}
|
||||
|
||||
func ExtractAnimatedSticker(data []byte) ([]byte, error) {
|
||||
zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read sticker zip: %w", err)
|
||||
}
|
||||
animationFile, err := zipReader.Open("animation/animation.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open animation.json: %w", err)
|
||||
}
|
||||
animationFileInfo, err := animationFile.Stat()
|
||||
if err != nil {
|
||||
_ = animationFile.Close()
|
||||
return nil, fmt.Errorf("failed to stat animation.json: %w", err)
|
||||
} else if animationFileInfo.Size() > uploadFileThreshold {
|
||||
_ = animationFile.Close()
|
||||
return nil, fmt.Errorf("animation.json is too large (%.2f MiB)", float64(animationFileInfo.Size())/1024/1024)
|
||||
}
|
||||
data, err = io.ReadAll(animationFile)
|
||||
_ = animationFile.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read animation.json: %w", err)
|
||||
}
|
||||
return data, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -20,14 +20,12 @@ import (
|
|||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"html"
|
||||
"html/template"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/exerrors"
|
||||
"go.mau.fi/util/exfmt"
|
||||
"go.mau.fi/util/ptr"
|
||||
"go.mau.fi/whatsmeow/proto/waAICommonDeprecated"
|
||||
"go.mau.fi/whatsmeow/proto/waE2E"
|
||||
|
|
@ -75,6 +73,72 @@ func (mc *MessageConverter) convertPlaceholderMessage(ctx context.Context, rawMs
|
|||
}
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) getHistoryReceiverName(ctx context.Context, receiver string) string {
|
||||
jid, err := types.ParseJID(receiver)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Str("receiver_jid", receiver).Msg("Failed to parse message history receiver JID")
|
||||
return receiver
|
||||
}
|
||||
_, displayname, err := mc.getBasicUserInfo(ctx, jid)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Stringer("receiver_jid", jid).Msg("Failed to resolve message history receiver")
|
||||
if jid.User != "" {
|
||||
return jid.User
|
||||
}
|
||||
return receiver
|
||||
}
|
||||
return displayname
|
||||
}
|
||||
|
||||
func messageHistoryStartTime(metadata *waE2E.MessageHistoryMetadata, fallback time.Time) time.Time {
|
||||
if metadata == nil {
|
||||
return fallback
|
||||
}
|
||||
if ts := metadata.GetOldestMessageTimestamp(); ts > 0 {
|
||||
return time.Unix(ts, 0).Local()
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) convertMessageHistoryShare(ctx context.Context, info *types.MessageInfo, metadata *waE2E.MessageHistoryMetadata, contextInfo *waE2E.ContextInfo) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
|
||||
names := make([]string, 0, len(metadata.GetHistoryReceivers()))
|
||||
for _, receiver := range metadata.GetHistoryReceivers() {
|
||||
if name := mc.getHistoryReceiverName(ctx, receiver); name != "" {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
|
||||
receivers := strings.Join(names, ", ")
|
||||
var fallback time.Time
|
||||
if info != nil {
|
||||
fallback = info.Timestamp
|
||||
}
|
||||
startAt := messageHistoryStartTime(metadata, fallback)
|
||||
body := "Message history shared."
|
||||
if !startAt.IsZero() {
|
||||
startTime := startAt.Format("Jan 2, 2006 at 3:04 PM")
|
||||
body = fmt.Sprintf("Message history shared starting on %s.", startTime)
|
||||
switch {
|
||||
case info != nil && info.IsFromMe && receivers != "":
|
||||
body = fmt.Sprintf("You sent %s message history that starts on %s.", receivers, startTime)
|
||||
case receivers != "":
|
||||
body = fmt.Sprintf("Sent %s message history that starts on %s.", receivers, startTime)
|
||||
}
|
||||
} else if info != nil && info.IsFromMe && receivers != "" {
|
||||
body = fmt.Sprintf("You sent %s message history.", receivers)
|
||||
} else if receivers != "" {
|
||||
body = fmt.Sprintf("Sent %s message history.", receivers)
|
||||
}
|
||||
|
||||
return &bridgev2.ConvertedMessagePart{
|
||||
Type: event.EventMessage,
|
||||
Content: &event.MessageEventContent{
|
||||
MsgType: event.MsgNotice,
|
||||
Body: body,
|
||||
},
|
||||
}, contextInfo
|
||||
}
|
||||
|
||||
const inviteMsg = `%s<hr/>This invitation to join "%s" expires at %s. Reply to this message with <code>%s accept</code> to accept the invite.`
|
||||
const inviteMsgBroken = `%s<hr/>This invitation to join "%s" expires at %s. However, the invite message is broken or unsupported and cannot be accepted.`
|
||||
const GroupInviteMetaField = "fi.mau.whatsapp.invite"
|
||||
|
|
@ -102,7 +166,7 @@ func (mc *MessageConverter) convertGroupInviteMessage(ctx context.Context, info
|
|||
}
|
||||
}
|
||||
|
||||
htmlMessage := fmt.Sprintf(template, event.TextToHTML(msg.GetCaption()), html.EscapeString(msg.GetGroupName()), expiry, mc.Bridge.Config.CommandPrefix)
|
||||
htmlMessage := fmt.Sprintf(template, event.TextToHTML(msg.GetCaption()), msg.GetGroupName(), expiry, mc.Bridge.Config.CommandPrefix)
|
||||
content := &event.MessageEventContent{
|
||||
MsgType: event.MsgText,
|
||||
Body: format.HTMLToText(htmlMessage),
|
||||
|
|
@ -119,92 +183,6 @@ func (mc *MessageConverter) convertGroupInviteMessage(ctx context.Context, info
|
|||
}, msg.GetContextInfo()
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) formatMessageHistoryNoticeJID(ctx context.Context, jid types.JID) string {
|
||||
_, displayName, err := mc.getBasicUserInfo(ctx, jid)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Stringer("jid", jid).Msg("Failed to get user info for message history notice")
|
||||
} else if displayName != "" {
|
||||
return displayName
|
||||
}
|
||||
switch jid.Server {
|
||||
case types.DefaultUserServer:
|
||||
return "+" + jid.User
|
||||
default:
|
||||
return "Unknown user " + jid.String()
|
||||
}
|
||||
}
|
||||
|
||||
const maxMessageHistoryNoticeReceivers = 5
|
||||
|
||||
var others = exfmt.Pluralizable("other")
|
||||
|
||||
func (mc *MessageConverter) formatMessageHistoryNoticeReceivers(ctx context.Context, receivers []string) string {
|
||||
receiverLimit := min(len(receivers), maxMessageHistoryNoticeReceivers)
|
||||
receiverNames := make([]string, 0, receiverLimit)
|
||||
for _, receiver := range receivers[:receiverLimit] {
|
||||
jid, err := types.ParseJID(receiver)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Str("receiver", receiver).Msg("Failed to parse message history receiver JID")
|
||||
receiverNames = append(receiverNames, receiver)
|
||||
} else {
|
||||
receiverNames = append(receiverNames, mc.formatMessageHistoryNoticeJID(ctx, jid))
|
||||
}
|
||||
}
|
||||
receiverText := strings.Join(receiverNames, ", ")
|
||||
if len(receivers) > receiverLimit {
|
||||
receiverText = fmt.Sprintf("%s + %s", receiverText, others(len(receivers)-receiverLimit))
|
||||
}
|
||||
return receiverText
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) messageHistoryNoticeLocation(ctx context.Context) *time.Location {
|
||||
portal := getPortal(ctx)
|
||||
loginID := portal.Receiver
|
||||
if loginID == "" {
|
||||
loginID = waid.MakeUserLoginID(getClient(ctx).Store.GetJID().ToNonAD())
|
||||
}
|
||||
if login := mc.Bridge.GetCachedUserLoginByID(loginID); login != nil {
|
||||
meta, _ := login.Metadata.(*waid.UserLoginMetadata)
|
||||
loc, err := meta.LoadTimezone()
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Str("timezone", meta.Timezone).Msg("Failed to load user timezone for message history notice")
|
||||
} else if loc != nil {
|
||||
return loc
|
||||
}
|
||||
}
|
||||
return time.Local
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) convertMessageHistoryNotice(ctx context.Context, info *types.MessageInfo, msg *waE2E.MessageHistoryNotice) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
|
||||
metadata := msg.GetMessageHistoryMetadata()
|
||||
|
||||
sender := mc.formatMessageHistoryNoticeJID(ctx, info.Sender)
|
||||
body := fmt.Sprintf("%s sent message history", sender)
|
||||
if receiverText := mc.formatMessageHistoryNoticeReceivers(ctx, metadata.GetHistoryReceivers()); receiverText != "" {
|
||||
body = fmt.Sprintf("%s to %s", body, receiverText)
|
||||
}
|
||||
if count := metadata.GetMessageCount(); count > 0 {
|
||||
messageWord := "messages"
|
||||
if count == 1 {
|
||||
messageWord = "message"
|
||||
}
|
||||
body = fmt.Sprintf("%s (%d %s)", body, count, messageWord)
|
||||
}
|
||||
if metadata != nil && metadata.OldestMessageTimestampInWindow != nil {
|
||||
oldestTS := time.Unix(metadata.GetOldestMessageTimestampInWindow(), 0).In(mc.messageHistoryNoticeLocation(ctx))
|
||||
body = fmt.Sprintf("%s, starting %s", body, oldestTS.Format("Jan 2, 2006 at 3:04 PM"))
|
||||
}
|
||||
body += "."
|
||||
|
||||
return &bridgev2.ConvertedMessagePart{
|
||||
Type: event.EventMessage,
|
||||
Content: &event.MessageEventContent{
|
||||
MsgType: event.MsgNotice,
|
||||
Body: body,
|
||||
},
|
||||
}, msg.GetContextInfo()
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) convertEphemeralSettingMessage(ctx context.Context, msg *waE2E.ProtocolMessage, ts time.Time, isBackfill bool) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
|
||||
portal := getPortal(ctx)
|
||||
portalMeta := portal.Metadata.(*waid.PortalMetadata)
|
||||
|
|
|
|||
|
|
@ -94,7 +94,31 @@ func (mc *MessageConverter) convertPollCreationMessage(ctx context.Context, msg
|
|||
}, msg.GetContextInfo()
|
||||
}
|
||||
|
||||
func rerouteMessageKey(ctx context.Context, chat, sender types.JID, groupLIDAddressing bool) types.JID {
|
||||
if store := getClient(ctx).Store; store != nil && chat.Server == types.DefaultUserServer && sender.Server == types.HiddenUserServer {
|
||||
senderPN, _ := store.LIDs.GetPNForLID(ctx, sender)
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Stringer("orig_participant", sender).
|
||||
Stringer("rerouted_participant", senderPN).
|
||||
Msg("Rerouting message key (PN recipient in LID DM)")
|
||||
if !senderPN.IsEmpty() {
|
||||
return senderPN
|
||||
}
|
||||
} else if store != nil && chat.Server == types.GroupServer && sender.Server == types.DefaultUserServer && groupLIDAddressing {
|
||||
senderLID, _ := store.LIDs.GetLIDForPN(ctx, sender)
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Stringer("orig_participant", sender).
|
||||
Stringer("rerouted_participant", senderLID).
|
||||
Msg("Rerouting message key (PN recipient in LID group)")
|
||||
if !senderLID.IsEmpty() {
|
||||
return senderLID
|
||||
}
|
||||
}
|
||||
return sender
|
||||
}
|
||||
|
||||
func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender types.JID, key *waCommon.MessageKey) networkid.MessageID {
|
||||
groupLIDAddressing := sender.Server == types.HiddenUserServer
|
||||
sender = sender.ToNonAD()
|
||||
var err error
|
||||
if !key.GetFromMe() {
|
||||
|
|
@ -107,7 +131,7 @@ func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender
|
|||
if sender.Server == types.LegacyUserServer {
|
||||
sender.Server = types.DefaultUserServer
|
||||
}
|
||||
} else if chat.Server == types.DefaultUserServer || chat.Server == types.HiddenUserServer || chat.Server == types.BotServer {
|
||||
} else if chat.Server == types.DefaultUserServer || chat.Server == types.BotServer {
|
||||
if sender.User == client.Store.GetJID().User || sender.User == client.Store.GetLID().User {
|
||||
// Message key is not from the sender, but message sender (containing key) is me,
|
||||
// so message key sender is the other user in the DM
|
||||
|
|
@ -115,12 +139,8 @@ func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender
|
|||
} else {
|
||||
// Message key is not from the sender, but message sender (containing key) is not me,
|
||||
// so message key sender is me
|
||||
if chat.Server == types.HiddenUserServer {
|
||||
sender = client.Store.GetLID().ToNonAD()
|
||||
} else {
|
||||
sender = client.Store.GetJID().ToNonAD()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
zerolog.Ctx(ctx).Warn().
|
||||
Stringer("chat", chat).
|
||||
|
|
@ -137,6 +157,10 @@ func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender
|
|||
chat = remoteJID
|
||||
}
|
||||
}
|
||||
sender = rerouteMessageKey(
|
||||
context.WithValue(ctx, contextKeyClient, client),
|
||||
chat, sender, groupLIDAddressing,
|
||||
)
|
||||
return waid.MakeMessageID(chat, sender, key.GetID())
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,455 +0,0 @@
|
|||
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
|
||||
// Copyright (C) 2026 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package msgconv
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/tidwall/gjson"
|
||||
"go.mau.fi/util/exstrings"
|
||||
"go.mau.fi/util/lottie"
|
||||
"go.mau.fi/util/random"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/database"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
"maunium.net/go/mautrix/event"
|
||||
|
||||
"go.mau.fi/mautrix-whatsapp/pkg/waid"
|
||||
)
|
||||
|
||||
func (mc *MessageConverter) GetCachedStickerPack(ctx context.Context, client *whatsmeow.Client, packID string) (*types.StickerPack, error) {
|
||||
mc.stickerPackCacheLock.Lock()
|
||||
defer mc.stickerPackCacheLock.Unlock()
|
||||
cached, ok := mc.stickerPackCache[packID]
|
||||
if ok {
|
||||
if cached == nil {
|
||||
return nil, bridgev2.RespError(mautrix.MNotFound.WithMessage("sticker pack not found (cached)"))
|
||||
}
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
pack, err := client.FetchStickerPack(ctx, packID)
|
||||
if errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) {
|
||||
mc.stickerPackCache[packID] = nil
|
||||
return nil, bridgev2.WrapRespErr(err, mautrix.MNotFound)
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mc.stickerPackCache[packID] = pack
|
||||
if packID != pack.StickerPackID {
|
||||
mc.stickerPackCache[pack.StickerPackID] = pack
|
||||
}
|
||||
return pack, nil
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) GetCachedSticker(ctx context.Context, client *whatsmeow.Client, packID string, hash []byte) (*types.StickerPackItem, error) {
|
||||
pack, err := mc.GetCachedStickerPack(ctx, client, packID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, sticker := range pack.Stickers {
|
||||
if bytes.Equal(sticker.FileHash, hash) {
|
||||
return sticker, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) DownloadImagePack(ctx context.Context, userLoginID networkid.UserLoginID, client *whatsmeow.Client, inputURL string) (*bridgev2.ImportedImagePack, error) {
|
||||
parsedURL, err := url.Parse(inputURL)
|
||||
if err != nil {
|
||||
return nil, bridgev2.WrapRespErr(err, mautrix.MNotFound)
|
||||
} else if parsedURL.Host != "api.whatsapp.com" && parsedURL.Host != "wa.me" {
|
||||
return nil, bridgev2.WrapRespErr(fmt.Errorf("invalid host %q", parsedURL.Host), mautrix.MNotFound)
|
||||
} else if !strings.HasPrefix(parsedURL.Path, "/stickerpack/") {
|
||||
return nil, bridgev2.WrapRespErr(fmt.Errorf("invalid path %q", parsedURL.Path), mautrix.MNotFound)
|
||||
}
|
||||
packName := strings.Split(strings.TrimPrefix(parsedURL.Path, "/stickerpack/"), "/")[0]
|
||||
if packName == "" {
|
||||
return nil, bridgev2.WrapRespErr(fmt.Errorf("empty pack name"), mautrix.MNotFound)
|
||||
}
|
||||
pack, err := mc.GetCachedStickerPack(ctx, client, packName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
canonicalURL := "https://wa.me/stickerpack/" + pack.StickerPackID
|
||||
topLevelExtra := map[string]any{
|
||||
"fi.mau.whatsapp.stickerpack": map[string]any{
|
||||
"id": pack.StickerPackID,
|
||||
"name": pack.Name,
|
||||
"description": pack.Description,
|
||||
"publisher": pack.Publisher,
|
||||
"animated": pack.Animated > 0,
|
||||
"lottie": pack.Lottie > 0,
|
||||
},
|
||||
}
|
||||
content := &event.ImagePackEventContent{
|
||||
Images: make(map[string]*event.ImagePackImage, len(pack.Stickers)),
|
||||
Metadata: event.ImagePackMetadata{
|
||||
DisplayName: pack.Name,
|
||||
AvatarURL: "",
|
||||
Usage: []event.ImagePackUsage{event.ImagePackUsageSticker},
|
||||
Attribution: fmt.Sprintf("By %s on WhatsApp %s", pack.Publisher, canonicalURL),
|
||||
BridgedPack: &event.BridgedStickerPack{
|
||||
Network: StickerSourceID,
|
||||
URL: canonicalURL,
|
||||
},
|
||||
},
|
||||
}
|
||||
ctx = context.WithValue(ctx, contextKeyClient, client)
|
||||
ctx = context.WithValue(ctx, contextKeyIntent, mc.Bridge.Bot)
|
||||
ctx = context.WithValue(ctx, contextKeyPortal, (*bridgev2.Portal)(nil))
|
||||
for i, sticker := range pack.Stickers {
|
||||
shortcode := sticker.PreviewWebpID
|
||||
if shortcode == "" {
|
||||
shortcode = fmt.Sprintf("%s_img%d", pack.StickerPackID, i+1)
|
||||
}
|
||||
body := sticker.AccessibilityText
|
||||
var emoji string
|
||||
if len(sticker.Emojis) > 0 {
|
||||
emoji = sticker.Emojis[0]
|
||||
if body == "" {
|
||||
body = strings.Join(sticker.Emojis, " ")
|
||||
}
|
||||
}
|
||||
part := &PreparedMedia{
|
||||
Type: event.EventSticker,
|
||||
MessageEventContent: &event.MessageEventContent{
|
||||
Body: body,
|
||||
Info: &event.FileInfo{
|
||||
MimeType: sticker.MimeType,
|
||||
Width: sticker.Width,
|
||||
Height: sticker.Height,
|
||||
Size: int(sticker.FileSize),
|
||||
BridgedSticker: &event.BridgedSticker{
|
||||
Network: StickerSourceID,
|
||||
ID: base64.StdEncoding.EncodeToString(sticker.FileHash),
|
||||
Emoji: emoji,
|
||||
PackURL: canonicalURL,
|
||||
},
|
||||
},
|
||||
},
|
||||
TypeDescription: "sticker",
|
||||
}
|
||||
dbKey := database.Key(fmt.Sprintf("stickercache:%x", part.Info.BridgedSticker.ID))
|
||||
fixStickerDimensions(part.Info)
|
||||
var packed *event.ImagePackImage
|
||||
if mc.DirectMedia {
|
||||
dbKey = ""
|
||||
if part.Info.MimeType == "application/was" {
|
||||
part.Info.MimeType = "video/lottie+json"
|
||||
}
|
||||
part.URL, err = mc.Bridge.Matrix.GenerateContentURI(ctx, waid.MakeStickerPackMediaID(pack.StickerPackID, sticker.FileHash, userLoginID))
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to generate content URI: %w", err))
|
||||
}
|
||||
} else if cached := mc.Bridge.DB.KV.Get(ctx, dbKey); cached != "" {
|
||||
err = json.Unmarshal([]byte(cached), &packed)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal cached sticker data: %w", err)
|
||||
}
|
||||
} else {
|
||||
err = mc.reuploadWhatsAppAttachment(ctx, sticker, part)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to reupload sticker %q: %w", sticker.GetDirectPath(), err)
|
||||
}
|
||||
}
|
||||
if packed == nil {
|
||||
packed = &event.ImagePackImage{
|
||||
URL: part.URL,
|
||||
Body: part.Body,
|
||||
Info: part.Info,
|
||||
}
|
||||
if dbKey != "" {
|
||||
data, _ := json.Marshal(packed)
|
||||
if data != nil {
|
||||
mc.Bridge.DB.KV.Set(ctx, dbKey, string(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
content.Images[shortcode] = packed
|
||||
}
|
||||
|
||||
return &bridgev2.ImportedImagePack{
|
||||
Content: content,
|
||||
Extra: topLevelExtra,
|
||||
Shortcode: pack.StickerPackID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type StickerMetadata struct {
|
||||
StickerPackID string `json:"sticker-pack-id"`
|
||||
AccessibilityText string `json:"accessibility-text"`
|
||||
Emojis []string `json:"emojis"`
|
||||
IsFirstPartySticker int `json:"is-first-party-sticker"`
|
||||
}
|
||||
|
||||
func (sm *StickerMetadata) ToMatrix(content *event.MessageEventContent) {
|
||||
if sm == nil {
|
||||
return
|
||||
}
|
||||
if sm.StickerPackID != "" && content.Info.BridgedSticker == nil {
|
||||
content.Info.BridgedSticker = &event.BridgedSticker{
|
||||
Network: StickerSourceID,
|
||||
PackURL: StickerPackURLPrefix + sm.StickerPackID,
|
||||
}
|
||||
if len(sm.Emojis) > 0 {
|
||||
content.Info.BridgedSticker.Emoji = sm.Emojis[0]
|
||||
}
|
||||
}
|
||||
if sm.AccessibilityText != "" {
|
||||
content.Body = sm.AccessibilityText
|
||||
} else if len(sm.Emojis) > 0 {
|
||||
content.Body = strings.Join(sm.Emojis, " ")
|
||||
}
|
||||
}
|
||||
|
||||
const StickerSourceID = "whatsapp"
|
||||
const StickerPackURLPrefix = "https://wa.me/stickerpack/"
|
||||
|
||||
func PackAnimatedSticker(data []byte) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
zipWriter := zip.NewWriter(&buf)
|
||||
f, err := zipWriter.Create("animation/animation.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create zip entry: %w", err)
|
||||
}
|
||||
_, err = f.Write(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to write zip entry: %w", err)
|
||||
}
|
||||
err = zipWriter.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to close zip writer: %w", err)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func ExtractAnimatedSticker(data []byte) ([]byte, *StickerMetadata, error) {
|
||||
zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read sticker zip: %w", err)
|
||||
}
|
||||
animationFile, err := zipReader.Open("animation/animation.json")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to open animation.json: %w", err)
|
||||
}
|
||||
animationFileInfo, err := animationFile.Stat()
|
||||
if err != nil {
|
||||
_ = animationFile.Close()
|
||||
return nil, nil, fmt.Errorf("failed to stat animation.json: %w", err)
|
||||
} else if animationFileInfo.Size() > uploadFileThreshold {
|
||||
_ = animationFile.Close()
|
||||
return nil, nil, fmt.Errorf("animation.json is too large (%.2f MiB)", float64(animationFileInfo.Size())/1024/1024)
|
||||
}
|
||||
data, err = io.ReadAll(animationFile)
|
||||
_ = animationFile.Close()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read animation.json: %w", err)
|
||||
}
|
||||
var meta StickerMetadata
|
||||
metaFile, err := zipReader.Open("animation/animation.json.overridden_metadata")
|
||||
if err == nil {
|
||||
_ = json.NewDecoder(metaFile).Decode(&meta)
|
||||
_ = metaFile.Close()
|
||||
}
|
||||
if meta.StickerPackID == "" {
|
||||
res := gjson.GetBytes(data, "metadata.customProps")
|
||||
if res.IsObject() {
|
||||
_ = json.Unmarshal(exstrings.UnsafeBytes(res.Raw), &meta)
|
||||
}
|
||||
}
|
||||
return data, &meta, nil
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) extractAnimatedSticker(fileInfo *PreparedMedia, data []byte) ([]byte, error) {
|
||||
data, meta, err := ExtractAnimatedSticker(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
meta.ToMatrix(fileInfo.MessageEventContent)
|
||||
fileInfo.Info.MimeType = "video/lottie+json"
|
||||
fileInfo.FileName = "sticker.json"
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) convertAnimatedSticker(ctx context.Context, fileInfo *PreparedMedia, data []byte) ([]byte, []byte, *event.FileInfo, error) {
|
||||
data, err := mc.extractAnimatedSticker(fileInfo, data)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
c := mc.AnimatedStickerConfig
|
||||
if c.Target == "disable" {
|
||||
return data, nil, nil, nil
|
||||
} else if !lottie.Supported() {
|
||||
zerolog.Ctx(ctx).Warn().Msg("Animated sticker conversion is enabled, but lottieconverter is not installed")
|
||||
return data, nil, nil, nil
|
||||
}
|
||||
input := bytes.NewReader(data)
|
||||
fileInfo.Info.MimeType = "image/" + c.Target
|
||||
fileInfo.FileName = "sticker." + c.Target
|
||||
switch c.Target {
|
||||
case "png":
|
||||
var output bytes.Buffer
|
||||
err = lottie.Convert(ctx, input, "", &output, c.Target, c.Args.Width, c.Args.Height, "1")
|
||||
return output.Bytes(), nil, nil, err
|
||||
case "gif":
|
||||
var output bytes.Buffer
|
||||
err = lottie.Convert(ctx, input, "", &output, c.Target, c.Args.Width, c.Args.Height, strconv.Itoa(c.Args.FPS))
|
||||
return output.Bytes(), nil, nil, err
|
||||
case "webm", "webp":
|
||||
tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("mautrix-whatsapp-lottieconverter-%s.%s", random.String(10), c.Target))
|
||||
defer func() {
|
||||
_ = os.Remove(tmpFile)
|
||||
}()
|
||||
thumbnailData, err := lottie.FFmpegConvert(ctx, input, tmpFile, c.Args.Width, c.Args.Height, c.Args.FPS)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
data, err = os.ReadFile(tmpFile)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("failed to read converted file: %w", err)
|
||||
}
|
||||
var thumbnailInfo *event.FileInfo
|
||||
if thumbnailData != nil {
|
||||
thumbnailInfo = &event.FileInfo{
|
||||
MimeType: "image/png",
|
||||
Width: c.Args.Width,
|
||||
Height: c.Args.Height,
|
||||
Size: len(thumbnailData),
|
||||
}
|
||||
}
|
||||
return data, thumbnailData, thumbnailInfo, nil
|
||||
default:
|
||||
return nil, nil, nil, fmt.Errorf("unsupported target format %s", c.Target)
|
||||
}
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) fillWebPStickerInfo(ctx context.Context, fileInfo *PreparedMedia, data []byte) {
|
||||
meta, err := extractWebPStickerMetadata(data)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Debug().Err(err).Msg("Failed to extract webp sticker metadata")
|
||||
return
|
||||
}
|
||||
meta.ToMatrix(fileInfo.MessageEventContent)
|
||||
}
|
||||
|
||||
// stickerMetadataEXIFTag is the custom EXIF tag WhatsApp uses to embed
|
||||
// sticker pack metadata as a JSON object inside non-animated webp stickers.
|
||||
const stickerMetadataEXIFTag = 0x5741
|
||||
|
||||
// extractWebPStickerMetadata parses the WhatsApp sticker pack metadata JSON
|
||||
// embedded in EXIF tag 0x5741 of a non-animated webp sticker.
|
||||
func extractWebPStickerMetadata(data []byte) (*StickerMetadata, error) {
|
||||
exif, err := findWebPChunk(data, "EXIF")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raw, err := findEXIFTagValue(exif, stickerMetadataEXIFTag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var meta StickerMetadata
|
||||
err = json.Unmarshal(raw, &meta)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse sticker metadata JSON: %w", err)
|
||||
}
|
||||
return &meta, nil
|
||||
}
|
||||
|
||||
func findWebPChunk(data []byte, chunkType string) ([]byte, error) {
|
||||
if len(data) < 12 || string(data[0:4]) != "RIFF" || string(data[8:12]) != "WEBP" {
|
||||
return nil, fmt.Errorf("not a webp file")
|
||||
}
|
||||
for pos := 12; pos+8 <= len(data); {
|
||||
size := binary.LittleEndian.Uint32(data[pos+4 : pos+8])
|
||||
start := pos + 8
|
||||
end := start + int(size)
|
||||
if end > len(data) {
|
||||
return nil, fmt.Errorf("webp chunk %q extends past end of file", data[pos:pos+4])
|
||||
}
|
||||
if string(data[pos:pos+4]) == chunkType {
|
||||
return data[start:end], nil
|
||||
}
|
||||
pos = end
|
||||
if pos%2 != 0 {
|
||||
pos++
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("webp chunk %q not found", chunkType)
|
||||
}
|
||||
|
||||
func findEXIFTagValue(exif []byte, tag uint16) ([]byte, error) {
|
||||
if len(exif) < 8 {
|
||||
return nil, fmt.Errorf("exif data too short")
|
||||
}
|
||||
var bo binary.ByteOrder
|
||||
switch string(exif[0:2]) {
|
||||
case "II":
|
||||
bo = binary.LittleEndian
|
||||
case "MM":
|
||||
bo = binary.BigEndian
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid TIFF byte order %q", exif[0:2])
|
||||
}
|
||||
if bo.Uint16(exif[2:4]) != 0x002A {
|
||||
return nil, fmt.Errorf("invalid TIFF magic")
|
||||
}
|
||||
ifdOffset := int(bo.Uint32(exif[4:8]))
|
||||
if ifdOffset < 0 || ifdOffset+2 > len(exif) {
|
||||
return nil, fmt.Errorf("IFD offset out of range")
|
||||
}
|
||||
count := int(bo.Uint16(exif[ifdOffset : ifdOffset+2]))
|
||||
entries := ifdOffset + 2
|
||||
if entries+count*12 > len(exif) {
|
||||
return nil, fmt.Errorf("IFD entries out of range")
|
||||
}
|
||||
for i := 0; i < count; i++ {
|
||||
entry := exif[entries+i*12 : entries+(i+1)*12]
|
||||
if bo.Uint16(entry[0:2]) != tag {
|
||||
continue
|
||||
}
|
||||
// Tag 0x5741 stores JSON as type 7 (UNDEFINED), where size == count bytes.
|
||||
size := int(bo.Uint32(entry[4:8]))
|
||||
if size <= 4 {
|
||||
return entry[8 : 8+size], nil
|
||||
}
|
||||
offset := int(bo.Uint32(entry[8:12]))
|
||||
if offset+size > len(exif) {
|
||||
return nil, fmt.Errorf("exif tag value out of range")
|
||||
}
|
||||
return exif[offset : offset+size], nil
|
||||
}
|
||||
return nil, fmt.Errorf("exif tag 0x%04x not found", tag)
|
||||
}
|
||||
|
|
@ -42,7 +42,6 @@ type UserLoginMetadata struct {
|
|||
AppStateRecoveryAttempted map[appstate.WAPatchName]time.Time `json:"app_state_recovery_attempted,omitempty"`
|
||||
|
||||
HistorySyncPortalsNeedCreating bool `json:"history_sync_portals_need_creating,omitempty"`
|
||||
ReachoutTimelockUntil time.Time `json:"reachout_timelock_until,omitempty"`
|
||||
|
||||
MData json.RawMessage `json:"mdata,omitempty"`
|
||||
}
|
||||
|
|
@ -62,13 +61,6 @@ func (m *UserLoginMetadata) GeneratePushKeys() {
|
|||
}
|
||||
}
|
||||
|
||||
func (m *UserLoginMetadata) LoadTimezone() (*time.Location, error) {
|
||||
if m == nil || m.Timezone == "" {
|
||||
return nil, nil
|
||||
}
|
||||
return time.LoadLocation(m.Timezone)
|
||||
}
|
||||
|
||||
type MessageErrorType string
|
||||
|
||||
const (
|
||||
|
|
|
|||
|
|
@ -83,13 +83,6 @@ func MakeMessageID(chat, sender types.JID, id types.MessageID) networkid.Message
|
|||
return networkid.MessageID(fmt.Sprintf("%s:%s:%s", chat.ToNonAD().String(), sender.ToNonAD().String(), id))
|
||||
}
|
||||
|
||||
func MakeMessageIDWithAltSender(chat, sender, altSender types.JID, id types.MessageID) networkid.MessageID {
|
||||
if chat.Server == types.HiddenUserServer && sender.Server == types.DefaultUserServer && altSender.Server == types.HiddenUserServer {
|
||||
sender = altSender
|
||||
}
|
||||
return MakeMessageID(chat, sender, id)
|
||||
}
|
||||
|
||||
func MakeFakeMessageID(chat, sender types.JID, data string) networkid.MessageID {
|
||||
return networkid.MessageID(fmt.Sprintf("fake:%s:%s:%s", chat.ToNonAD().String(), sender.ToNonAD().String(), data))
|
||||
}
|
||||
|
|
@ -98,7 +91,6 @@ type ParsedMessageID struct {
|
|||
Chat types.JID
|
||||
Sender types.JID
|
||||
ID types.MessageID
|
||||
Version []byte
|
||||
}
|
||||
|
||||
func (pmi *ParsedMessageID) String() networkid.MessageID {
|
||||
|
|
|
|||
|
|
@ -33,10 +33,9 @@ const (
|
|||
mediaIDTypeMessage = 255
|
||||
mediaIDTypeAvatar = 254
|
||||
mediaIDTypeCommunityAvatar = 253
|
||||
mediaIDTypeStickerPackItem = 252
|
||||
)
|
||||
|
||||
func MakeMediaID(messageInfo *types.MessageInfo, idOverride types.MessageID, receiver networkid.UserLoginID, version []byte) networkid.MediaID {
|
||||
func MakeMediaID(messageInfo *types.MessageInfo, idOverride types.MessageID, receiver networkid.UserLoginID) networkid.MediaID {
|
||||
compactChat := compactJID(messageInfo.Chat.ToNonAD())
|
||||
compactSender := compactJID(messageInfo.Sender.ToNonAD())
|
||||
receiverID := compactJID(ParseUserLoginID(receiver, 0))
|
||||
|
|
@ -46,7 +45,7 @@ func MakeMediaID(messageInfo *types.MessageInfo, idOverride types.MessageID, rec
|
|||
} else {
|
||||
compactID = compactMsgID(messageInfo.ID)
|
||||
}
|
||||
mediaID := make([]byte, 0, 6+len(compactChat)+len(compactSender)+len(receiverID)+len(compactID)+len(version))
|
||||
mediaID := make([]byte, 0, 5+len(compactChat)+len(compactSender)+len(receiverID)+len(compactID))
|
||||
mediaID = append(mediaID, mediaIDTypeMessage)
|
||||
mediaID = append(mediaID, byte(len(compactChat)))
|
||||
mediaID = append(mediaID, compactChat...)
|
||||
|
|
@ -56,8 +55,6 @@ func MakeMediaID(messageInfo *types.MessageInfo, idOverride types.MessageID, rec
|
|||
mediaID = append(mediaID, receiverID...)
|
||||
mediaID = append(mediaID, byte(len(compactID)))
|
||||
mediaID = append(mediaID, compactID...)
|
||||
mediaID = append(mediaID, byte(len(version)))
|
||||
mediaID = append(mediaID, version...)
|
||||
return mediaID
|
||||
}
|
||||
|
||||
|
|
@ -85,28 +82,9 @@ type AvatarMediaInfo struct {
|
|||
Community bool
|
||||
}
|
||||
|
||||
func MakeStickerPackMediaID(packID string, fileHash []byte, receiver networkid.UserLoginID) networkid.MediaID {
|
||||
receiverID := compactJID(ParseUserLoginID(receiver, 0))
|
||||
mediaID := make([]byte, 0, 4+len(packID)+len(fileHash)+len(receiverID))
|
||||
mediaID = append(mediaID, mediaIDTypeStickerPackItem)
|
||||
mediaID = append(mediaID, byte(len(packID)))
|
||||
mediaID = append(mediaID, packID...)
|
||||
mediaID = append(mediaID, byte(len(fileHash)))
|
||||
mediaID = append(mediaID, fileHash...)
|
||||
mediaID = append(mediaID, byte(len(receiverID)))
|
||||
mediaID = append(mediaID, receiverID...)
|
||||
return mediaID
|
||||
}
|
||||
|
||||
type StickerPackMediaInfo struct {
|
||||
PackID string
|
||||
FileHash []byte
|
||||
}
|
||||
|
||||
type ParsedMediaID struct {
|
||||
Message *ParsedMessageID
|
||||
Avatar *AvatarMediaInfo
|
||||
Sticker *StickerPackMediaInfo
|
||||
UserLogin networkid.UserLoginID
|
||||
}
|
||||
|
||||
|
|
@ -140,12 +118,6 @@ func ParseMediaID(mediaID networkid.MediaID) (*ParsedMediaID, error) {
|
|||
Sender: senderJID,
|
||||
ID: id,
|
||||
}
|
||||
if len(mediaID) > 0 {
|
||||
parsed.Message.Version, err = readCompact(&mediaID, rawBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse version: %w", err)
|
||||
}
|
||||
}
|
||||
parsed.UserLogin = MakeUserLoginID(receiverID)
|
||||
case mediaIDTypeAvatar, mediaIDTypeCommunityAvatar:
|
||||
targetJID, err := readCompact(&mediaID, parseCompactJID)
|
||||
|
|
@ -166,24 +138,6 @@ func ParseMediaID(mediaID networkid.MediaID) (*ParsedMediaID, error) {
|
|||
Community: mediaIDType == mediaIDTypeCommunityAvatar,
|
||||
}
|
||||
parsed.UserLogin = MakeUserLoginID(receiverID)
|
||||
case mediaIDTypeStickerPackItem:
|
||||
packID, err := readCompact(&mediaID, parseString)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse sticker pack ID: %w", err)
|
||||
}
|
||||
fileHash, err := readCompact(&mediaID, rawBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse sticker file hash: %w", err)
|
||||
}
|
||||
receiverID, err := readCompact(&mediaID, parseCompactJID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse receiver JID: %w", err)
|
||||
}
|
||||
parsed.Sticker = &StickerPackMediaInfo{
|
||||
PackID: packID,
|
||||
FileHash: fileHash,
|
||||
}
|
||||
parsed.UserLogin = MakeUserLoginID(receiverID)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown media ID type %d", mediaIDType)
|
||||
}
|
||||
|
|
@ -292,10 +246,6 @@ func parseCompactJID(jid []byte) (types.JID, error) {
|
|||
}
|
||||
}
|
||||
|
||||
func rawBytes(data []byte) ([]byte, error) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func readCompact[T any](data *networkid.MediaID, fn func(data []byte) (T, error)) (T, error) {
|
||||
var defVal T
|
||||
if len(*data) < 1 {
|
||||
|
|
|
|||
Loading…
Reference in a new issue