mirror of
https://github.com/mautrix/whatsapp.git
synced 2026-11-09 15:49:18 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd7f229ca6 |
60 changed files with 1173 additions and 5170 deletions
|
|
@ -1 +0,0 @@
|
|||
code_format_allowlist: ["wa-connect-failure-%d"]
|
||||
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.
|
||||
-->
|
||||
|
|
|
|||
9
.github/workflows/go.yml
vendored
9
.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.24", "1.25"]
|
||||
name: Lint ${{ matrix.go-version == '1.25' && '(latest)' || '(old)' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v7
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
cache: true
|
||||
|
|
@ -30,7 +30,6 @@ jobs:
|
|||
run: |
|
||||
go install golang.org/x/tools/cmd/goimports@latest
|
||||
go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
go install github.com/beeper/pre-commit-go/bridgelint@latest
|
||||
export PATH="$HOME/go/bin:$PATH"
|
||||
|
||||
- name: Run pre-commit
|
||||
|
|
|
|||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
lock-stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v6
|
||||
- uses: dessant/lock-threads@v5
|
||||
id: lock
|
||||
with:
|
||||
issue-inactive-days: 90
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ repos:
|
|||
- id: check-added-large-files
|
||||
|
||||
- repo: https://github.com/tekwizely/pre-commit-golang
|
||||
rev: v1.0.0-rc.4
|
||||
rev: v1.0.0-rc.2
|
||||
hooks:
|
||||
- id: go-imports-repo
|
||||
args:
|
||||
|
|
@ -21,12 +21,9 @@ repos:
|
|||
- id: go-mod-tidy
|
||||
|
||||
- repo: https://github.com/beeper/pre-commit-go
|
||||
rev: v0.5.0
|
||||
rev: v0.4.2
|
||||
hooks:
|
||||
- id: zerolog-ban-msgf
|
||||
- id: zerolog-use-stringer
|
||||
- id: prevent-literal-http-methods
|
||||
- id: zerolog-ban-global-log
|
||||
- id: bridgelint
|
||||
language: system
|
||||
verbose: true
|
||||
|
|
|
|||
134
CHANGELOG.md
134
CHANGELOG.md
|
|
@ -1,137 +1,3 @@
|
|||
# v26.10
|
||||
|
||||
* Added support for new rich response messages used by the Muse AI bot.
|
||||
* Added support for bridging custom profile set for Muse.
|
||||
* Added automatic one-time resync of keys needed to decrypt Muse messages.
|
||||
* Added support for blocking and unblocking users on WhatsApp.
|
||||
* Added support for contact array messages (thanks to [@voska] in [#961]).
|
||||
* Added partial support for filling quoted message when sending replies to
|
||||
WhatsApp.
|
||||
* Changed undecryptable message resends to only bridge as edits and never as new
|
||||
messages, to avoid races if the message was deleted before the resend.
|
||||
* Changed login to wait for OTKs to be successfully uploaded, as the login will
|
||||
fail if the bridge is stopped before that point.
|
||||
* Changed chat info syncing to not bridge mute status if app state sync is
|
||||
failing.
|
||||
* Changed power levels to allow poll votes in read-only groups even if the room
|
||||
is encrypted.
|
||||
* Fixed backfilled media retries not getting sent if configured for local time.
|
||||
* Fixed "delete for me" from WhatsApp not being bridged correctly in DMs.
|
||||
* Fixed very long WhatsApp messages not being bridged.
|
||||
* Fixed document message caption edits not being handled correctly.
|
||||
|
||||
[@voska]: https://github.com/voska
|
||||
[#961]: https://github.com/mautrix/whatsapp/pull/961
|
||||
|
||||
# v26.09
|
||||
|
||||
* Bumped minimum Go version to 1.26.
|
||||
* Added option to limit eager member sync in group chat portals.
|
||||
* Added option to fetch avatars lazily when using direct media.
|
||||
* Added one-time migration to ensure LID ghost avatars are consistent with the
|
||||
old phone number ghosts.
|
||||
* Fixed the resolve identifier command and API not returning any user info for
|
||||
users who hadn't previously been encountered.
|
||||
* Fixed group portal power levels to always allow poll responses.
|
||||
* Fixed handling history sync events where the phone sends nonsensical
|
||||
timestamps.
|
||||
* Fixed handling edits to HD media captions.
|
||||
* Fixed handling bridging own read receipts from the native apps in channels.
|
||||
* Fixed decrypting messages from the new Muse AI bot
|
||||
* May require resyncing app state using `!wa sync appstate regular_high`.
|
||||
* The message contents aren't supported yet.
|
||||
|
||||
# v26.08
|
||||
|
||||
* Switched direct chats to use LIDs instead of phone numbers.
|
||||
|
||||
# v26.07
|
||||
|
||||
* Updated Docker image to Alpine 3.24.
|
||||
* Added support for the extra passkey authentication step when logging in.
|
||||
* Added support for newer versions of poll messages from WhatsApp.
|
||||
* Fixed poll votes in DMs.
|
||||
|
||||
# v26.06
|
||||
|
||||
* Added placeholder for group message history share notices.
|
||||
* Fixed community spaces not being bridged properly in some cases.
|
||||
* Fixed handling edits of HD media.
|
||||
* Fixed duplicate message when a message from another own device is initially
|
||||
undecryptable and later gets resolved.
|
||||
|
||||
# v26.05
|
||||
|
||||
* Added support for importing sticker packs from WhatsApp.
|
||||
* Added support for WhatsApp's new message edit encryption scheme.
|
||||
|
||||
# v26.04
|
||||
|
||||
* Added support for @room mentions in both directions.
|
||||
* Changed initial backfill to happen even if WhatsApp doesn't send full history.
|
||||
* Fixed panic when handling updates to unknown polls from WhatsApp.
|
||||
* Fixed some background loops not stopping when a user is logged out.
|
||||
|
||||
# v26.03
|
||||
|
||||
* Added option to save outgoing messages in the database to allow encryption
|
||||
retries to work across restarts.
|
||||
* Fixed contact list API not returning some contacts.
|
||||
* Fixed business template messages with media duplicating the text part.
|
||||
|
||||
# v26.02
|
||||
|
||||
* Bumped minimum Go version to 1.25.
|
||||
* Added automatic recovery for WhatsApp app state sync issues.
|
||||
* Fixed LID redirects for some non-message events.
|
||||
|
||||
# v26.01
|
||||
|
||||
* Fixed broadcast list messages to LIDs causing split DMs.
|
||||
* Fixed read receipts not working correctly in LID DMs.
|
||||
* Fixed backfill sometimes racing with receiving LID mappings.
|
||||
|
||||
# v25.12
|
||||
|
||||
* Updated Docker image to Alpine 3.23.
|
||||
* Fixed group member invites from Matrix not automatically disinviting the phone
|
||||
number ghost when the invite is redirected to a LID ghost.
|
||||
|
||||
# v25.11
|
||||
|
||||
* Added interface support for notifying about failed invites when creating a
|
||||
group and sending the invites via DM (only applicable to provisioning API).
|
||||
* Added migration to automatically delete duplicate LID DM portals that were
|
||||
created earlier.
|
||||
* Changed contact list API to only include actual phone contacts.
|
||||
* Removed extra unrecognized message notice when receiving live photos
|
||||
(bridging the live photo video is not currently planned).
|
||||
* Fixed pairing not working with latest WhatsApp Android version.
|
||||
* Fixed replies, read receipts and typing notifications not being bridged
|
||||
correctly after DM LID migration.
|
||||
* Fixed backfill creating duplicate portals if history sync contains both LID
|
||||
and phone number DM data.
|
||||
* Fixed some cases of LID and phone number user infos getting out of sync.
|
||||
* Fixed muting chat forever not being bridged correctly from WhatsApp.
|
||||
* Fixed old mutes being re-applied on chat resync in some cases.
|
||||
* Fixed backfilling failing if some reactions were missing sender info.
|
||||
* Fixed space not being deleted when leaving community on WhatsApp.
|
||||
* Fixed sticker size metadata on Matrix not matching how native WhatsApp Web
|
||||
renders them.
|
||||
* Fixed ratelimit errors in login not being exposed to the user properly
|
||||
(thanks to [@dead8309] in [#852]).
|
||||
|
||||
[@dead8309]: https://github.com/dead8309
|
||||
[#852]: https://github.com/mautrix/whatsapp/pull/852
|
||||
|
||||
# v25.10
|
||||
|
||||
* Switched to calendar versioning.
|
||||
* Added support for bridging event edits.
|
||||
* Fixed backfill creating incorrect disappearing timer change notices.
|
||||
* Fixed previous messages not being marked as read when sending a new message.
|
||||
* Fixed incoming call notices with LID addressing going into different DM room.
|
||||
|
||||
# v0.12.5 (2025-09-16)
|
||||
|
||||
* Removed legacy provisioning API and database legacy migration.
|
||||
|
|
|
|||
|
|
@ -1 +0,0 @@
|
|||
See <https://docs.mau.fi/bridges/general/contributing.html>
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1-alpine3.24 AS builder
|
||||
FROM golang:1-alpine3.22 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.22
|
||||
|
||||
ENV UID=1337 \
|
||||
GID=1337
|
||||
|
|
|
|||
|
|
@ -1,6 +1,4 @@
|
|||
ARG DOCKER_HUB="docker.io"
|
||||
|
||||
FROM ${DOCKER_HUB}/alpine:3.24
|
||||
FROM alpine:3.22
|
||||
|
||||
ENV UID=1337 \
|
||||
GID=1337
|
||||
|
|
|
|||
4
build.sh
4
build.sh
|
|
@ -1,2 +1,4 @@
|
|||
#!/bin/sh
|
||||
BINARY_NAME=mautrix-whatsapp go tool maubuild "$@"
|
||||
MAUTRIX_VERSION=$(cat go.mod | grep 'maunium.net/go/mautrix ' | awk '{ print $2 }')
|
||||
GO_LDFLAGS="-s -w -X main.Tag=$(git describe --exact-match --tags 2>/dev/null) -X main.Commit=$(git rev-parse HEAD) -X 'main.BuildTime=`date -Iseconds`' -X 'maunium.net/go/mautrix.GoModVersion=$MAUTRIX_VERSION'"
|
||||
go build -ldflags="$GO_LDFLAGS" "$@" ./cmd/mautrix-whatsapp
|
||||
|
|
|
|||
112
cmd/mautrix-whatsapp/legacyprovision.go
Normal file
112
cmd/mautrix-whatsapp/legacyprovision.go
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog/hlog"
|
||||
"go.mau.fi/util/exhttp"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"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).Device.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,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
|
@ -18,12 +18,18 @@ var m = mxmain.BridgeMain{
|
|||
Name: "mautrix-whatsapp",
|
||||
URL: "https://github.com/mautrix/whatsapp",
|
||||
Description: "A Matrix-WhatsApp puppeting bridge.",
|
||||
Version: "26.10",
|
||||
SemCalVer: true,
|
||||
Version: "0.12.5",
|
||||
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.InitVersion(Tag, Commit, BuildTime)
|
||||
m.Run()
|
||||
}
|
||||
|
|
|
|||
56
go.mod
56
go.mod
|
|
@ -1,53 +1,51 @@
|
|||
module go.mau.fi/mautrix-whatsapp
|
||||
|
||||
go 1.26.0
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.27.2
|
||||
|
||||
tool go.mau.fi/util/cmd/maubuild
|
||||
toolchain go1.25.1
|
||||
|
||||
require (
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/lib/pq v1.12.3
|
||||
github.com/rs/zerolog v1.35.1
|
||||
github.com/tidwall/gjson v1.20.0
|
||||
github.com/yuin/goldmark v1.8.6
|
||||
go.mau.fi/util v0.11.0
|
||||
go.mau.fi/webp v0.3.0
|
||||
go.mau.fi/whatsmeow v0.0.0-20261026125351-1fbae988179a
|
||||
golang.org/x/image v0.47.0
|
||||
golang.org/x/net v0.61.0
|
||||
golang.org/x/sync v0.24.0
|
||||
google.golang.org/protobuf v1.36.12
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/rs/zerolog v1.34.0
|
||||
go.mau.fi/util v0.9.1
|
||||
go.mau.fi/webp v0.2.0
|
||||
go.mau.fi/whatsmeow v0.0.0-20250919124702-c8bdfd36d05e
|
||||
golang.org/x/image v0.31.0
|
||||
golang.org/x/net v0.44.0
|
||||
golang.org/x/sync v0.17.0
|
||||
google.golang.org/protobuf v1.36.9
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
maunium.net/go/mautrix v0.32.1-0.20261105141340-842796acf445
|
||||
maunium.net/go/mautrix v0.25.2-0.20250919113047-b760023dcaa3
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/beeper/argo-go v1.1.2 // indirect
|
||||
github.com/coder/websocket v1.8.15 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.7.0 // indirect
|
||||
github.com/coder/websocket v1.8.14 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.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.52 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20260918085751-abfca077860b // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490 // 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/match v1.2.0 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // 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
|
||||
go.mau.fi/libsignal v0.2.2 // indirect
|
||||
github.com/yuin/goldmark v1.7.13 // indirect
|
||||
go.mau.fi/libsignal v0.2.0 // indirect
|
||||
go.mau.fi/zeroconfig v0.2.0 // indirect
|
||||
golang.org/x/crypto v0.58.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e // indirect
|
||||
golang.org/x/mod v0.42.0 // indirect
|
||||
golang.org/x/sys v0.49.0 // indirect
|
||||
golang.org/x/text v0.43.0 // indirect
|
||||
golang.org/x/crypto v0.42.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250911091902-df9299821621 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/text v0.29.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
|
||||
|
|
|
|||
114
go.sum
114
go.sum
|
|
@ -1,5 +1,5 @@
|
|||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
|
||||
|
|
@ -8,17 +8,22 @@ 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 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
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/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
|
|
@ -26,36 +31,41 @@ 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.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
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.52 h1:wVbm2Qnf4OXkqhBTSPuCRZDRnxfbVrrmiCEroVdog8U=
|
||||
github.com/mattn/go-sqlite3 v1.14.52/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
|
||||
github.com/petermattis/goid v0.0.0-20260918085751-abfca077860b h1:OzNsuVdSWGwXvWKTtChx9ve89k4cFJ6NxYM4Aw4/f+E=
|
||||
github.com/petermattis/goid v0.0.0-20260918085751-abfca077860b/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490 h1:QTvNkZ5ylY0PGgA+Lih+GdboMLY/G9SEGLMEGVjTVA4=
|
||||
github.com/petermattis/goid v0.0.0-20250904145737-900bdf8bb490/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=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/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=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/stretchr/testify v1.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI=
|
||||
github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw=
|
||||
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.20.0 h1:+agJ3rEzKcCXKDKo0ml26UROcpcAlnZyjq+TjbY5Fto=
|
||||
github.com/tidwall/gjson v1.20.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 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
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=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
|
|
@ -63,37 +73,37 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
|||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=
|
||||
github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
|
||||
github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4=
|
||||
github.com/yuin/goldmark v1.8.6/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.11.0 h1:JNEpkVRxfTdJUC0DIGKiRlLKzl4G/qLX/eVzoP7QuwY=
|
||||
go.mau.fi/util v0.11.0/go.mod h1:04AEFVsc4zNdIkDC1gkdqbg/nZFolHY50pyXh2DSy7s=
|
||||
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-20261026125351-1fbae988179a h1:ZyCV/mWfRkutxs5R0K3v+oyTbF4huprSlPZg/LEFQ7k=
|
||||
go.mau.fi/whatsmeow v0.0.0-20261026125351-1fbae988179a/go.mod h1:iY4lm61nTLWsNiIE1EmHUZwhKGBeoV4dhnVE2uMtNoQ=
|
||||
github.com/yuin/goldmark v1.7.13 h1:GPddIs617DnBLFFVJFgpo1aBfe/4xcvMc3SB5t/D0pA=
|
||||
github.com/yuin/goldmark v1.7.13/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
go.mau.fi/libsignal v0.2.0 h1:oRXj3OHhEJq51BFEM8/50UZblmWiTYH93hsNTPcbk90=
|
||||
go.mau.fi/libsignal v0.2.0/go.mod h1:tvjoDsMejgT38CXTXwqaYu8itBiY8O2Mb6biWvZBb9k=
|
||||
go.mau.fi/util v0.9.1 h1:A+XKHRsjKkFi2qOm4RriR1HqY2hoOXNS3WFHaC89r2Y=
|
||||
go.mau.fi/util v0.9.1/go.mod h1:M0bM9SyaOWJniaHs9hxEzz91r5ql6gYq6o1q5O1SsjQ=
|
||||
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-20250919124702-c8bdfd36d05e h1:+o9+K5Qyo/490H4NyPVA0Es+aEzmmjaK37/437hV2LM=
|
||||
go.mau.fi/whatsmeow v0.0.0-20250919124702-c8bdfd36d05e/go.mod h1:dvltpCF0rOHbbur25DHbQ3Ovi747z2Pm11S2M7p1T74=
|
||||
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.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
|
||||
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
|
||||
golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e h1:HgI6W1OolKK6tcV74is+rl9SMT0njsHlp0TbQiGXI/A=
|
||||
golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e/go.mod h1:DBoxwWhjaW8DJJw/YeM4zpeOKMC1FMXSblsYy+1mFSw=
|
||||
golang.org/x/image v0.47.0 h1:za/8gVz++PPZqmZJBNSISICN5Rrj5zixP/8K++fgmqo=
|
||||
golang.org/x/image v0.47.0/go.mod h1:IkN5o4A7vq55vXN8FWjjnhg9Rs2GOtffqsSi9mWAKYE=
|
||||
golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg=
|
||||
golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs=
|
||||
golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw=
|
||||
golang.org/x/net v0.61.0/go.mod h1:WgDCOTH8iwtB66T4/GfI94AHo/hhdb7Hah9NDseQfiI=
|
||||
golang.org/x/sync v0.24.0 h1:iqiDrGoDgyuvAKrhLQthUEzU/R378N78gXbBjxed7oA=
|
||||
golang.org/x/sync v0.24.0/go.mod h1:sUUOizhqBxiL6pEWpqNLUiaJn1ShEbZ6BBqskPbjZm0=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/exp v0.0.0-20250911091902-df9299821621 h1:2id6c1/gto0kaHYyrixvknJ8tUK/Qs5IsmBtrc+FtgU=
|
||||
golang.org/x/exp v0.0.0-20250911091902-df9299821621/go.mod h1:TwQYMMnGpvZyc+JpB/UAuTNIsVJifOlSkrZkhcvpVUk=
|
||||
golang.org/x/image v0.31.0 h1:mLChjE2MV6g1S7oqbXC0/UcKijjm5fnJLUYKIYrLESA=
|
||||
golang.org/x/image v0.31.0/go.mod h1:R9ec5Lcp96v9FTF+ajwaH3uGxPH4fKfHHAVbUILxghA=
|
||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
|
||||
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
||||
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
|
||||
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
|
||||
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.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
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=
|
||||
|
|
@ -103,5 +113,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.32.1-0.20261105141340-842796acf445 h1:ZI9Y2YEmjqQDloOCiciHeynTRYjUwK0ZZbNcRH/5NOc=
|
||||
maunium.net/go/mautrix v0.32.1-0.20261105141340-842796acf445/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU=
|
||||
maunium.net/go/mautrix v0.25.2-0.20250919113047-b760023dcaa3 h1:MU7lyDtxVAsd1Mh+WGJOFD5PsGLRRl3Xv67zizxLkgc=
|
||||
maunium.net/go/mautrix v0.25.2-0.20250919113047-b760023dcaa3/go.mod h1:iSueLJ/2fBaNrsTObGqi1j0cl/loxrtAjmjay1scYD8=
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"runtime/debug"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
|
@ -13,8 +12,6 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/exerrors"
|
||||
"go.mau.fi/util/exmaps"
|
||||
"go.mau.fi/util/ptr"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/proto/waE2E"
|
||||
|
|
@ -64,8 +61,13 @@ func (wa *WhatsAppClient) historySyncLoop(ctx context.Context) {
|
|||
}
|
||||
}()
|
||||
for {
|
||||
var resetTimer, errored bool
|
||||
var resetTimer bool
|
||||
select {
|
||||
case evt := <-wa.historySyncs:
|
||||
// The timer is stopped unconditionally and restarted if either handleWAHistorySync had conversations,
|
||||
// or if the timer was previously started and hadn't reached the loop above yet.
|
||||
dispatchTimer.Stop()
|
||||
resetTimer, _ = wa.handleWAHistorySync(ctx, evt, false)
|
||||
case <-wa.historySyncWakeup:
|
||||
dispatchTimer.Stop()
|
||||
notif, rowid, err := wa.Main.DB.HSNotif.GetNext(ctx, wa.UserLogin.ID)
|
||||
|
|
@ -74,21 +76,12 @@ func (wa *WhatsAppClient) historySyncLoop(ctx context.Context) {
|
|||
} else if notif == nil {
|
||||
wa.UserLogin.Log.Debug().Msg("No more queued history sync notifications")
|
||||
} else {
|
||||
resetTimer, errored = wa.downloadAndSaveWAHistorySyncData(ctx, notif, rowid)
|
||||
resetTimer = wa.downloadAndSaveWAHistorySyncData(ctx, notif, rowid)
|
||||
// Continue waking up the loop until all queued notifications are processed
|
||||
select {
|
||||
case wa.historySyncWakeup <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
if errored {
|
||||
wa.UserLogin.Log.Debug().Msg("Temporarily stopping history sync loop after error")
|
||||
select {
|
||||
case <-time.After(1 * time.Minute):
|
||||
case <-ctx.Done():
|
||||
wa.UserLogin.Log.Debug().Msg("Stopping main history sync loop")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
case <-ctx.Done():
|
||||
wa.UserLogin.Log.Debug().Msg("Stopping main history sync loop")
|
||||
|
|
@ -120,54 +113,21 @@ func (wa *WhatsAppClient) saveWAHistorySyncNotification(ctx context.Context, evt
|
|||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context, evt *waE2E.HistorySyncNotification, rowid int) (resetTimer, errored bool) {
|
||||
func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context, evt *waE2E.HistorySyncNotification, rowid int) (resetTimer bool) {
|
||||
log := wa.UserLogin.Log.With().
|
||||
Str("action", "download history sync").
|
||||
Stringer("sync_type", evt.GetSyncType()).
|
||||
Uint32("chunk_order", evt.GetChunkOrder()).
|
||||
Uint32("progress", evt.GetProgress()).
|
||||
Logger()
|
||||
defer func() {
|
||||
if v := recover(); v != nil {
|
||||
log.Err(exerrors.RecoverToError(v)).
|
||||
Bytes(zerolog.ErrorStackFieldName, debug.Stack()).
|
||||
Msg("History sync saving panicked")
|
||||
errored = true
|
||||
}
|
||||
}()
|
||||
log.Debug().
|
||||
Int64("oldest_msg_in_chunk_ts", evt.GetOldestMsgInChunkTimestampSec()).
|
||||
Any("full_request_meta", evt.GetFullHistorySyncOnDemandRequestMetadata()).
|
||||
Any("access_status", evt.GetMessageAccessStatus()).
|
||||
Str("peer_data_request_session_id", evt.GetPeerDataRequestSessionID()).
|
||||
Msg("Downloading history sync")
|
||||
log.Debug().Msg("Downloading history sync")
|
||||
blob, err := wa.Client.DownloadHistorySync(log.WithContext(ctx), evt, true)
|
||||
if errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith403) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith410) {
|
||||
log.Err(err).Msg("History sync blob is no longer available, dropping notification")
|
||||
if err = wa.Main.DB.HSNotif.Delete(ctx, rowid); err != nil {
|
||||
log.Err(err).Msg("Failed to delete queued history sync notification")
|
||||
errored = true
|
||||
}
|
||||
return
|
||||
} else if err != nil {
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to download history sync")
|
||||
errored = true
|
||||
return
|
||||
}
|
||||
if blob.GetSyncType() == waHistorySync.HistorySync_ON_DEMAND {
|
||||
wa.finishOnDemandHistorySyncRequest(evt.GetPeerDataRequestSessionID())
|
||||
wa.handleOnDemandHistorySync(ctx, blob)
|
||||
if err = wa.Main.DB.HSNotif.Delete(ctx, rowid); err != nil {
|
||||
log.Err(err).Msg("Failed to delete queued on-demand history sync notification")
|
||||
} else if err = wa.Client.DeleteMedia(ctx, whatsmeow.MediaHistory, evt.GetDirectPath(), evt.GetFileEncSHA256(), evt.GetEncHandle()); err != nil {
|
||||
log.Err(err).Msg("Failed to delete history sync blob from server")
|
||||
} else {
|
||||
log.Debug().Msg("Finished handling on-demand history sync and deleted history sync blob from server")
|
||||
}
|
||||
return
|
||||
}
|
||||
err = wa.Main.DB.DoTxn(ctx, nil, func(ctx context.Context) (innerErr error) {
|
||||
innerErr = wa.handleWAHistorySync(ctx, evt, blob, true)
|
||||
resetTimer, innerErr = wa.handleWAHistorySync(ctx, blob, true)
|
||||
if innerErr != nil {
|
||||
return
|
||||
}
|
||||
|
|
@ -179,29 +139,13 @@ func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context,
|
|||
})
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to store history sync notification data")
|
||||
errored = true
|
||||
} else {
|
||||
resetTimer = blob.GetSyncType() == waHistorySync.HistorySync_INITIAL_BOOTSTRAP ||
|
||||
blob.GetSyncType() == waHistorySync.HistorySync_RECENT ||
|
||||
blob.GetSyncType() == waHistorySync.HistorySync_FULL
|
||||
err = wa.Client.DeleteMedia(ctx, whatsmeow.MediaHistory, evt.GetDirectPath(), evt.GetFileEncSHA256(), evt.GetEncHandle())
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to delete history sync blob from server")
|
||||
} else {
|
||||
log.Debug().Msg("Deleted history sync blob from server")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWAHistorySync(
|
||||
ctx context.Context,
|
||||
notif *waE2E.HistorySyncNotification,
|
||||
evt *waHistorySync.HistorySync,
|
||||
stopOnError bool,
|
||||
) error {
|
||||
func (wa *WhatsAppClient) handleWAHistorySync(ctx context.Context, evt *waHistorySync.HistorySync, stopOnError bool) (bool, error) {
|
||||
if evt == nil || evt.SyncType == nil {
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
log := wa.UserLogin.Log.With().
|
||||
Str("action", "store history sync").
|
||||
|
|
@ -226,71 +170,41 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
|||
Int("recent_sticker_count", len(evt.GetRecentStickers())).
|
||||
Int("past_participant_count", len(evt.GetPastParticipants())).
|
||||
Msg("Ignoring history sync")
|
||||
return nil
|
||||
return false, nil
|
||||
}
|
||||
log.Info().
|
||||
Int("conversation_count", len(evt.GetConversations())).
|
||||
Int("past_participant_count", len(evt.GetPastParticipants())).
|
||||
Dict("notification_metadata", zerolog.Dict().
|
||||
Int64("oldest_msg_in_chunk_ts", notif.GetOldestMsgInChunkTimestampSec()).
|
||||
Any("full_request_meta", notif.GetFullHistorySyncOnDemandRequestMetadata()).
|
||||
Any("access_status", notif.GetMessageAccessStatus()).
|
||||
Str("peer_data_request_session_id", notif.GetPeerDataRequestSessionID())).
|
||||
Msg("Storing history sync")
|
||||
start := time.Now()
|
||||
successfullySavedTotal := 0
|
||||
failedToSaveTotal := 0
|
||||
totalMessageCount := 0
|
||||
for _, conv := range evt.GetConversations() {
|
||||
log := log.With().
|
||||
Int("msg_count", len(conv.GetMessages())).
|
||||
Logger()
|
||||
jid, err := types.ParseJID(conv.GetID())
|
||||
if err != nil {
|
||||
totalMessageCount += len(conv.GetMessages())
|
||||
log.Warn().Err(err).
|
||||
Str("chat_jid", conv.GetID()).
|
||||
Int("msg_count", len(conv.GetMessages())).
|
||||
Msg("Failed to parse chat JID in history sync")
|
||||
continue
|
||||
} else if jid.Server == types.BroadcastServer {
|
||||
log.Debug().Stringer("chat_jid", jid).Msg("Skipping broadcast list in history sync")
|
||||
continue
|
||||
} else if jid == types.PSAJID {
|
||||
// New messages in the official updates chat aren't bridged because they use a different path,
|
||||
// so drop history as well. This should be removed if the PSA chat gets properly bridged in the future.
|
||||
log.Debug().Stringer("chat_jid", jid).Msg("Skipping PSA in history sync")
|
||||
continue
|
||||
} else {
|
||||
totalMessageCount += len(conv.GetMessages())
|
||||
}
|
||||
if jid == types.MetaAIJID {
|
||||
jid = types.NewMetaAIJID
|
||||
} else if jid.Server == types.DefaultUserServer {
|
||||
lid, err := wa.GetStore().LIDs.GetLIDForPN(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")
|
||||
} else {
|
||||
log.Debug().
|
||||
Stringer("lid", lid).
|
||||
Stringer("pn", jid).
|
||||
Msg("Rerouting phone number DM to LID in history sync")
|
||||
jid = lid
|
||||
}
|
||||
}
|
||||
log.UpdateContext(func(c zerolog.Context) zerolog.Context {
|
||||
return c.Stringer("chat_jid", jid)
|
||||
})
|
||||
totalMessageCount += len(conv.GetMessages())
|
||||
log := log.With().
|
||||
Stringer("chat_jid", jid).
|
||||
Int("msg_count", len(conv.GetMessages())).
|
||||
Logger()
|
||||
|
||||
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 {
|
||||
|
|
@ -298,43 +212,23 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
|||
Int("msg_index", i).
|
||||
Str("msg_id", rawMsg.GetMessage().GetKey().GetID()).
|
||||
Uint64("msg_time_seconds", rawMsg.GetMessage().GetMessageTimestamp()).
|
||||
Stringer("stub_type", rawMsg.GetMessage().GetMessageStubType()).
|
||||
Str("message_type", getMessageType(rawMsg.GetMessage().GetMessage())).
|
||||
Bool("from_me", rawMsg.GetMessage().GetKey().GetFromMe()).
|
||||
Msg("Dropping historical message due to parse error")
|
||||
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).
|
||||
|
|
@ -349,8 +243,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()).
|
||||
|
|
@ -359,17 +255,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
|
||||
|
|
@ -377,7 +271,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)
|
||||
|
|
@ -387,7 +281,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")
|
||||
}
|
||||
|
|
@ -399,20 +293,15 @@ 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) {
|
||||
log := wa.UserLogin.Log.With().
|
||||
Str("action", "create portals from history sync").
|
||||
Logger()
|
||||
defer func() {
|
||||
if v := recover(); v != nil {
|
||||
log.Err(exerrors.RecoverToError(v)).
|
||||
Bytes(zerolog.ErrorStackFieldName, debug.Stack()).
|
||||
Msg("History sync portal creation panicked")
|
||||
}
|
||||
}()
|
||||
ctx = log.WithContext(ctx)
|
||||
limit := wa.Main.Config.HistorySync.MaxInitialConversations
|
||||
loginTS := wa.UserLogin.Metadata.(*waid.UserLoginMetadata).LoggedInAt
|
||||
|
|
@ -453,9 +342,9 @@ func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) {
|
|||
log.Warn().Err(ctx.Err()).Msg("Context cancelled, stopping history sync portal creation")
|
||||
return
|
||||
}
|
||||
wrappedInfo, err := wa.getChatInfo(ctx, conv.ChatJID, conv, true)
|
||||
if errors.Is(err, whatsmeow.ErrNotInGroup) || errors.Is(err, whatsmeow.ErrGroupNotFound) || errors.Is(err, ErrNewsletterNotFound) {
|
||||
log.Debug().Err(err).Stringer("chat_jid", conv.ChatJID).
|
||||
wrappedInfo, err := wa.getChatInfo(ctx, conv.ChatJID, conv)
|
||||
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 {
|
||||
|
|
@ -528,73 +417,40 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if portalJID.Server == types.DefaultUserServer {
|
||||
zerolog.Ctx(ctx).Warn().Stringer("portal_jid", portalJID).Msg("FetchMessages called for phone number portal")
|
||||
}
|
||||
var markRead bool
|
||||
var startTime, endTime *time.Time
|
||||
var conv *wadb.Conversation
|
||||
if params.Forward || wa.Main.Config.HistorySync.BackwardsOnDemand {
|
||||
conv, err = wa.Main.DB.Conversation.Get(ctx, wa.UserLogin.ID, portalJID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get conversation from database: %w", err)
|
||||
}
|
||||
}
|
||||
if params.Forward {
|
||||
if params.AnchorMessage != nil {
|
||||
startTime = ptr.Ptr(params.AnchorMessage.Timestamp)
|
||||
}
|
||||
if conv != nil {
|
||||
conv, err := wa.Main.DB.Conversation.Get(ctx, wa.UserLogin.ID, portalJID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get conversation from database: %w", err)
|
||||
} else if conv != nil {
|
||||
markRead = !ptr.Val(conv.MarkedAsUnread) && ptr.Val(conv.UnreadCount) == 0
|
||||
}
|
||||
} else {
|
||||
if params.AnchorMessage != nil {
|
||||
endTime = ptr.Ptr(params.AnchorMessage.Timestamp)
|
||||
} else if params.Cursor != "" {
|
||||
endTimeUnix, err := strconv.ParseInt(string(params.Cursor), 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse cursor: %w", err)
|
||||
}
|
||||
if params.Cursor != "" {
|
||||
endTimeUnix, err := strconv.ParseInt(string(params.Cursor), 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse cursor: %w", err)
|
||||
}
|
||||
cursorTime := time.Unix(endTimeUnix, 0)
|
||||
if endTime == nil || cursorTime.Before(*endTime) {
|
||||
endTime = &cursorTime
|
||||
}
|
||||
}
|
||||
}
|
||||
var anchorID types.MessageID
|
||||
if params.AnchorMessage != nil {
|
||||
parsedID, _ := waid.ParseMessageID(params.AnchorMessage.ID)
|
||||
if parsedID != nil {
|
||||
anchorID = parsedID.ID
|
||||
}
|
||||
}
|
||||
var hasMore bool
|
||||
if !params.Forward && wa.Main.Config.HistorySync.BackwardsOnDemand {
|
||||
hasMore = conv != nil && ptr.Val(conv.EndOfHistoryTransferType) == waHistorySync.Conversation_COMPLETE_BUT_MORE_MESSAGES_REMAIN_ON_PRIMARY
|
||||
endTime = ptr.Ptr(time.Unix(endTimeUnix, 0))
|
||||
} else if params.AnchorMessage != nil {
|
||||
endTime = ptr.Ptr(params.AnchorMessage.Timestamp)
|
||||
}
|
||||
messages, err := wa.Main.DB.Message.GetBetween(ctx, wa.UserLogin.ID, portalJID, startTime, endTime, params.Count+1)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load messages from database: %w", err)
|
||||
} else if len(messages) == 0 || (len(messages) == 1 && anchorID != "" && messages[0].GetKey().GetID() == anchorID) {
|
||||
wa.deleteHistorySyncMessages(ctx, portalJID, 0, 0)
|
||||
hasMore = hasMore && params.AnchorMessage != nil
|
||||
if hasMore && !params.AllowSlowFetch {
|
||||
return &bridgev2.FetchMessagesResponse{
|
||||
MoreRequiresSlowFetch: true,
|
||||
HasMore: true,
|
||||
Forward: params.Forward,
|
||||
}, nil
|
||||
} else if hasMore {
|
||||
return wa.fetchMessagesFromPhone(ctx, portalJID, params)
|
||||
}
|
||||
} else if len(messages) == 0 {
|
||||
return &bridgev2.FetchMessagesResponse{
|
||||
HasMore: false,
|
||||
Forward: params.Forward,
|
||||
}, nil
|
||||
}
|
||||
hasMore := false
|
||||
oldestTS := messages[len(messages)-1].GetMessageTimestamp()
|
||||
newestTS := messages[0].GetMessageTimestamp()
|
||||
if len(messages) > params.Count {
|
||||
oldestTS := messages[len(messages)-1].GetMessageTimestamp()
|
||||
hasMore = true
|
||||
// For safety, cut off messages with the oldest timestamp in the response.
|
||||
// Otherwise, if there are multiple messages with the same timestamp, the next fetch may miss some.
|
||||
|
|
@ -605,118 +461,19 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
|
|||
}
|
||||
}
|
||||
}
|
||||
resp, err := wa.convertHistorySyncMessages(ctx, params.Portal, portalJID, messages, true, !params.Forward && !hasMore)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert messages: %w", err)
|
||||
}
|
||||
// TODO might be better to always set true here and let GetBetween return an empty result to mark as false?
|
||||
resp.HasMore = hasMore
|
||||
resp.Forward = params.Forward
|
||||
resp.MarkRead = markRead
|
||||
if params.Forward && !markRead && conv != nil && !ptr.Val(conv.MarkedAsUnread) {
|
||||
resp.ReadUpTo = historySyncReadUpTo(resp.Messages, int(ptr.Val(conv.UnreadCount)))
|
||||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func historySyncReadUpTo(messages []*bridgev2.BackfillMessage, unreadCount int) time.Time {
|
||||
if unreadCount <= 0 {
|
||||
return time.Time{}
|
||||
}
|
||||
for i := len(messages) - 1; i >= 0; i-- {
|
||||
if messages[i].Sender.IsFromMe {
|
||||
continue
|
||||
}
|
||||
unreadCount--
|
||||
if unreadCount == 0 {
|
||||
return messages[i].Timestamp.Add(-time.Nanosecond)
|
||||
}
|
||||
}
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) deleteHistorySyncMessages(ctx context.Context, portalJID types.JID, newestTS, oldestTS uint64) {
|
||||
var err error
|
||||
var rows int64
|
||||
if (newestTS == 0 && oldestTS == 0) || !wa.Main.Bridge.Config.Backfill.Queue.AnyEnabled() {
|
||||
// If the backfill queue isn't enabled, delete all messages after backfilling a batch.
|
||||
rows, err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, portalJID)
|
||||
} else {
|
||||
// Otherwise just delete the messages that got backfilled
|
||||
rows, err = wa.Main.DB.Message.DeleteBetween(ctx, wa.UserLogin.ID, portalJID, newestTS, oldestTS)
|
||||
}
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).
|
||||
Stringer("portal_jid", portalJID).
|
||||
Uint64("newest_ts", newestTS).
|
||||
Uint64("oldest_ts", oldestTS).
|
||||
Msg("Failed to delete messages from database after backfill")
|
||||
} else {
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Stringer("portal_jid", portalJID).
|
||||
Uint64("newest_ts", newestTS).
|
||||
Uint64("oldest_ts", oldestTS).
|
||||
Int64("rows_affected", rows).
|
||||
Msg("Deleted history sync messages from database")
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) convertHistorySyncMessages(
|
||||
ctx context.Context,
|
||||
portal *bridgev2.Portal,
|
||||
portalJID types.JID,
|
||||
messages []*waWeb.WebMessageInfo,
|
||||
explodeOnError bool,
|
||||
deleteEverything bool,
|
||||
) (*bridgev2.FetchMessagesResponse, error) {
|
||||
cli := wa.Client
|
||||
if cli == nil {
|
||||
return nil, bridgev2.ErrNotLoggedIn
|
||||
}
|
||||
oldestTS := messages[len(messages)-1].GetMessageTimestamp()
|
||||
newestTS := messages[0].GetMessageTimestamp()
|
||||
convertedMessages := make([]*bridgev2.BackfillMessage, 0, len(messages))
|
||||
convertedMessages := make([]*bridgev2.BackfillMessage, len(messages))
|
||||
var mediaRequests []*wadb.MediaRequest
|
||||
dups := make(exmaps.Set[networkid.MessageID])
|
||||
for i, msg := range messages {
|
||||
evt, err := cli.ParseWebMessage(portalJID, msg)
|
||||
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)
|
||||
}
|
||||
if evt.Info.IsFromMe {
|
||||
wa.addPreviousOwnLID(ctx, evt.Info.Sender)
|
||||
// This should never happen because the info is already parsed once before being stored in the database
|
||||
return nil, fmt.Errorf("failed to parse info of message %s: %w", msg.GetKey().GetID(), err)
|
||||
}
|
||||
var mediaReq *wadb.MediaRequest
|
||||
isViewOnce := evt.IsViewOnce || evt.IsViewOnceV2 || evt.IsViewOnceV2Extension
|
||||
converted, mediaReq := wa.convertHistorySyncMessage(
|
||||
ctx, cli, 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)
|
||||
}
|
||||
|
|
@ -725,11 +482,23 @@ 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() {
|
||||
if deleteEverything {
|
||||
wa.deleteHistorySyncMessages(ctx, portalJID, 1<<62, oldestTS)
|
||||
// TODO this only deletes after backfilling. If there's no need for backfill after a relogin,
|
||||
// the messages will be stuck in the database
|
||||
var err error
|
||||
if !wa.Main.Bridge.Config.Backfill.Queue.Enabled && !wa.Main.Bridge.Config.Backfill.WillPaginateManually {
|
||||
// If the backfill queue isn't enabled, delete all messages after backfilling a batch.
|
||||
err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, portalJID)
|
||||
} else {
|
||||
wa.deleteHistorySyncMessages(ctx, portalJID, newestTS, oldestTS)
|
||||
// Otherwise just delete the messages that got backfilled
|
||||
err = wa.Main.DB.Message.DeleteBetween(ctx, wa.UserLogin.ID, portalJID, newestTS, oldestTS)
|
||||
}
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to delete messages from database after backfill")
|
||||
}
|
||||
if len(mediaRequests) > 0 {
|
||||
go func(ctx context.Context) {
|
||||
|
|
@ -748,164 +517,8 @@ 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")
|
||||
}
|
||||
anchor := params.AnchorMessage
|
||||
parsed, err := waid.ParseMessageID(anchor.ID)
|
||||
if err != nil {
|
||||
anchor, err = wa.Main.Bridge.DB.Message.GetFirstPortalMessageExcludingIDPattern(ctx, params.Portal.PortalKey, "fake:%")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get first non-fake portal message: %w", err)
|
||||
} else if anchor == nil {
|
||||
return &bridgev2.FetchMessagesResponse{}, nil
|
||||
}
|
||||
parsed, err = waid.ParseMessageID(anchor.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse anchor message ID: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
msgID := wa.Client.GenerateMessageID()
|
||||
if !wa.startOnDemandHistorySyncRequest(ctx, portalJID, msgID) {
|
||||
return &bridgev2.FetchMessagesResponse{
|
||||
HasMore: true,
|
||||
Pending: true,
|
||||
}, nil
|
||||
}
|
||||
reqData := wa.Client.BuildHistorySyncRequest(&types.MessageInfo{
|
||||
MessageSource: types.MessageSource{
|
||||
Chat: portalJID,
|
||||
Sender: parsed.Sender,
|
||||
IsFromMe: wa.IsOwnJID(parsed.Sender),
|
||||
IsGroup: parsed.Chat.Server == types.GroupServer,
|
||||
},
|
||||
ID: parsed.ID,
|
||||
Timestamp: anchor.Timestamp,
|
||||
}, 50)
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Str("request_msg_id", msgID).
|
||||
Stringer("portal_jid", portalJID).
|
||||
Any("anchor_msg_parsed", parsed).
|
||||
Any("request_data", reqData).
|
||||
Msg("Sending history sync request")
|
||||
_, err = wa.Client.SendMessage(ctx, wa.JID.ToNonAD(), reqData, whatsmeow.SendRequestExtra{
|
||||
ID: msgID,
|
||||
Peer: true,
|
||||
})
|
||||
if err != nil {
|
||||
wa.onDemandHistorySyncLock.Lock()
|
||||
delete(wa.onDemandHistorySyncRequests, portalJID)
|
||||
wa.onDemandHistorySyncLock.Unlock()
|
||||
return nil, fmt.Errorf("failed to send history sync request: %w", err)
|
||||
}
|
||||
return &bridgev2.FetchMessagesResponse{
|
||||
HasMore: true,
|
||||
Pending: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type onDemandHistorySyncRequest struct {
|
||||
ID types.MessageID
|
||||
SentAt time.Time
|
||||
}
|
||||
|
||||
const onDemandHistorySyncRequestTimeout = 1 * time.Hour
|
||||
|
||||
func (wa *WhatsAppClient) startOnDemandHistorySyncRequest(ctx context.Context, portalJID types.JID, msgID types.MessageID) bool {
|
||||
wa.onDemandHistorySyncLock.Lock()
|
||||
defer wa.onDemandHistorySyncLock.Unlock()
|
||||
if req, inFlight := wa.onDemandHistorySyncRequests[portalJID]; inFlight && time.Since(req.SentAt) < onDemandHistorySyncRequestTimeout {
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Str("in_flight_request_msg_id", req.ID).
|
||||
Stringer("portal_jid", portalJID).
|
||||
Time("sent_at", req.SentAt).
|
||||
Msg("Not sending history sync request, previous request is still in flight")
|
||||
return false
|
||||
}
|
||||
wa.onDemandHistorySyncRequests[portalJID] = onDemandHistorySyncRequest{ID: msgID, SentAt: time.Now()}
|
||||
return true
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) finishOnDemandHistorySyncRequest(sessionID string) {
|
||||
wa.onDemandHistorySyncLock.Lock()
|
||||
defer wa.onDemandHistorySyncLock.Unlock()
|
||||
for chatJID, req := range wa.onDemandHistorySyncRequests {
|
||||
if req.ID == sessionID {
|
||||
delete(wa.onDemandHistorySyncRequests, chatJID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleOnDemandHistorySync(ctx context.Context, blob *waHistorySync.HistorySync) {
|
||||
if len(blob.GetConversations()) > 1 {
|
||||
zerolog.Ctx(ctx).Warn().
|
||||
Int("conversation_count", len(blob.GetConversations())).
|
||||
Msg("Received on-demand history sync with multiple conversations")
|
||||
}
|
||||
for _, conv := range blob.GetConversations() {
|
||||
portalJID, err := types.ParseJID(conv.GetID())
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Str("jid", conv.GetID()).Msg("Failed to parse portal JID")
|
||||
continue
|
||||
}
|
||||
if portalJID == types.MetaAIJID {
|
||||
portalJID = types.NewMetaAIJID
|
||||
} else if portalJID.Server == types.DefaultUserServer {
|
||||
lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, portalJID)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Stringer("lid", portalJID).Msg("Failed to get LID for phone number in on-demand history sync")
|
||||
} else if lid.IsEmpty() {
|
||||
zerolog.Ctx(ctx).Warn().Stringer("lid", portalJID).Msg("No LID found for phone number in on-demand history sync")
|
||||
} else {
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Stringer("lid", lid).
|
||||
Stringer("pn", portalJID).
|
||||
Msg("Rerouting phone number DM to LID in on-demand history sync")
|
||||
portalJID = lid
|
||||
}
|
||||
}
|
||||
portal, err := wa.Main.Bridge.GetPortalByKey(ctx, wa.makeWAPortalKey(portalJID))
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Stringer("portal_jid", portalJID).Msg("Failed to get portal for on-demand history sync")
|
||||
continue
|
||||
}
|
||||
ctx := zerolog.Ctx(ctx).With().
|
||||
Str("portal_id", string(portal.ID)).
|
||||
Str("portal_receiver", string(portal.Receiver)).
|
||||
Stringer("portal_mxid", portal.MXID).
|
||||
Logger().WithContext(ctx)
|
||||
portal.HandleRemoteBackfill(ctx, wa.UserLogin, &simplevent.Backfill{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventBackfill,
|
||||
PortalKey: portal.PortalKey,
|
||||
},
|
||||
GetDataFunc: func(ctx context.Context, portal *bridgev2.Portal) (*bridgev2.FetchMessagesResponse, error) {
|
||||
if len(conv.GetMessages()) == 0 {
|
||||
return &bridgev2.FetchMessagesResponse{}, nil
|
||||
}
|
||||
messages := make([]*waWeb.WebMessageInfo, len(conv.GetMessages()))
|
||||
for i, rawMsg := range conv.GetMessages() {
|
||||
messages[i] = rawMsg.Message
|
||||
}
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Int("message_count", len(messages)).
|
||||
Stringer("end_of_history_type", conv.GetEndOfHistoryTransferType()).
|
||||
Msg("Converting messages to bridge from on-demand history sync")
|
||||
resp, err := wa.convertHistorySyncMessages(ctx, portal, portalJID, messages, false, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.HasMore = conv.GetEndOfHistoryTransferType() == waHistorySync.Conversation_COMPLETE_BUT_MORE_MESSAGES_REMAIN_ON_PRIMARY
|
||||
return resp, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) convertHistorySyncMessage(
|
||||
ctx context.Context, cli *whatsmeow.Client, portal *bridgev2.Portal, info *types.MessageInfo, msg, rawMsg *waE2E.Message, isViewOnce bool, reactions []*waWeb.Reaction,
|
||||
ctx context.Context, portal *bridgev2.Portal, info *types.MessageInfo, msg, rawMsg *waE2E.Message, isViewOnce bool, reactions []*waWeb.Reaction,
|
||||
) (*bridgev2.BackfillMessage, *wadb.MediaRequest) {
|
||||
// New messages turn these into edits, but in backfill we only have the last version,
|
||||
// so no need to do the edit thing. Instead, just unwrap the message.
|
||||
|
|
@ -914,35 +527,34 @@ 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, cli, intent, msg, rawMsg, info, isViewOnce, true, nil),
|
||||
Sender: wa.makeEventSender(ctx, pickLID(info.Sender, info.SenderAlt)),
|
||||
ID: msgID,
|
||||
TxnID: networkid.TransactionID(msgID),
|
||||
ConvertedMessage: wa.Main.MsgConv.ToMatrix(ctx, portal, wa.Client, intent, msg, rawMsg, info, isViewOnce, true, nil),
|
||||
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)),
|
||||
Reactions: make([]*bridgev2.BackfillReaction, len(reactions)),
|
||||
}
|
||||
mediaReq := wa.processFailedMedia(ctx, portal.PortalKey, wrapped.ID, wrapped.ConvertedMessage, true)
|
||||
for _, reaction := range reactions {
|
||||
for i, 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() {
|
||||
continue
|
||||
}
|
||||
wrapped.Reactions = append(wrapped.Reactions, &bridgev2.BackfillReaction{
|
||||
wrapped.Reactions[i] = &bridgev2.BackfillReaction{
|
||||
TargetPart: ptr.Ptr(networkid.PartID("")),
|
||||
Timestamp: time.UnixMilli(reaction.GetSenderTimestampMS()),
|
||||
Sender: wa.makeEventSender(ctx, sender),
|
||||
Emoji: reaction.GetText(),
|
||||
})
|
||||
}
|
||||
}
|
||||
return wrapped, mediaReq
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import (
|
|||
"go.mau.fi/util/jsontime"
|
||||
"go.mau.fi/util/ptr"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/database"
|
||||
"maunium.net/go/mautrix/event"
|
||||
|
||||
"go.mau.fi/mautrix-whatsapp/pkg/waid"
|
||||
|
|
@ -19,7 +18,6 @@ var WhatsAppGeneralCaps = &bridgev2.NetworkGeneralCapabilities{
|
|||
AggressiveUpdateInfo: true,
|
||||
ImplicitReadReceipts: true,
|
||||
Provisioning: bridgev2.ProvisioningCapabilities{
|
||||
ImagePackImport: true,
|
||||
ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{
|
||||
CreateDM: true,
|
||||
LookupPhone: true,
|
||||
|
|
@ -52,7 +50,7 @@ func (wa *WhatsAppConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilit
|
|||
}
|
||||
|
||||
func (wa *WhatsAppConnector) GetBridgeInfoVersion() (info, caps int) {
|
||||
return 1, 10
|
||||
return 1, 4
|
||||
}
|
||||
|
||||
const WAMaxFileSize = 2000 * 1024 * 1024
|
||||
|
|
@ -67,7 +65,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel {
|
|||
}
|
||||
|
||||
func capID() string {
|
||||
base := "fi.mau.whatsapp.capabilities.2026_10_01"
|
||||
base := "fi.mau.whatsapp.capabilities.2025_08_25+1"
|
||||
if ffmpeg.Supported() {
|
||||
return base + "+ffmpeg"
|
||||
}
|
||||
|
|
@ -126,10 +124,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,
|
||||
|
|
@ -163,56 +161,26 @@ var whatsappCaps = &event.RoomFeatures{
|
|||
MaxSize: WAMaxFileSize,
|
||||
},
|
||||
},
|
||||
State: event.StateFeatureMap{
|
||||
event.StateRoomName.Type: {Level: event.CapLevelFullySupported},
|
||||
event.StateRoomAvatar.Type: {Level: event.CapLevelFullySupported},
|
||||
event.StateTopic.Type: {Level: event.CapLevelFullySupported},
|
||||
event.StateBeeperDisappearingTimer.Type: {Level: event.CapLevelFullySupported},
|
||||
},
|
||||
MemberActions: event.MemberFeatureMap{
|
||||
event.MemberActionInvite: event.CapLevelFullySupported,
|
||||
event.MemberActionKick: event.CapLevelFullySupported,
|
||||
event.MemberActionLeave: event.CapLevelFullySupported,
|
||||
},
|
||||
MaxTextLength: MaxTextLength,
|
||||
LocationMessage: event.CapLevelFullySupported,
|
||||
Poll: event.CapLevelFullySupported,
|
||||
PollEnd: event.CapLevelUnsupported,
|
||||
PollHiddenVotes: event.CapLevelUnsupported,
|
||||
PollDuplicateOptions: event.CapLevelUnsupported,
|
||||
PollMaxOptions: 12,
|
||||
PollOptionMaxLength: 100,
|
||||
Reply: event.CapLevelFullySupported,
|
||||
Edit: event.CapLevelFullySupported,
|
||||
EditMaxAge: ptr.Ptr(jsontime.S(EditMaxAge)),
|
||||
Delete: event.CapLevelFullySupported,
|
||||
DeleteForMe: false,
|
||||
DeleteMaxAge: ptr.Ptr(jsontime.S(2 * 24 * time.Hour)),
|
||||
Reaction: event.CapLevelFullySupported,
|
||||
ReactionCount: 1,
|
||||
ReadReceipts: true,
|
||||
TypingNotifications: true,
|
||||
DisappearingTimer: waDisappearingCap,
|
||||
DeleteChat: true,
|
||||
MaxTextLength: MaxTextLength,
|
||||
LocationMessage: event.CapLevelFullySupported,
|
||||
Poll: event.CapLevelFullySupported,
|
||||
Reply: event.CapLevelFullySupported,
|
||||
Edit: event.CapLevelFullySupported,
|
||||
EditMaxCount: 10,
|
||||
EditMaxAge: ptr.Ptr(jsontime.S(EditMaxAge)),
|
||||
Delete: event.CapLevelFullySupported,
|
||||
DeleteForMe: false,
|
||||
DeleteMaxAge: ptr.Ptr(jsontime.S(2 * 24 * time.Hour)),
|
||||
Reaction: event.CapLevelFullySupported,
|
||||
ReactionCount: 1,
|
||||
ReadReceipts: true,
|
||||
TypingNotifications: true,
|
||||
DisappearingTimer: waDisappearingCap,
|
||||
}
|
||||
|
||||
var whatsappDMCaps *event.RoomFeatures
|
||||
var whatsappCAGCaps *event.RoomFeatures
|
||||
var whatsappNoteToSelfCaps *event.RoomFeatures
|
||||
|
||||
func init() {
|
||||
whatsappDMCaps = ptr.Clone(whatsappCaps)
|
||||
whatsappDMCaps.ID = capID() + "+dm"
|
||||
whatsappDMCaps.State = event.StateFeatureMap{
|
||||
event.StateBeeperDisappearingTimer.Type: {Level: event.CapLevelFullySupported},
|
||||
}
|
||||
whatsappDMCaps.BlockUser = true
|
||||
whatsappDMCaps.MemberActions = nil
|
||||
|
||||
whatsappNoteToSelfCaps = ptr.Clone(whatsappDMCaps)
|
||||
whatsappNoteToSelfCaps.ID = capID() + "+self"
|
||||
whatsappNoteToSelfCaps.BlockUser = false
|
||||
|
||||
whatsappCAGCaps = ptr.Clone(whatsappCaps)
|
||||
whatsappCAGCaps.ID = capID() + "+cag"
|
||||
whatsappCAGCaps.Reply = event.CapLevelUnsupported
|
||||
|
|
@ -222,12 +190,6 @@ func init() {
|
|||
func (wa *WhatsAppClient) GetCapabilities(ctx context.Context, portal *bridgev2.Portal) *event.RoomFeatures {
|
||||
if portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup {
|
||||
return whatsappCAGCaps
|
||||
} else if portal.RoomType == database.RoomTypeDM {
|
||||
jid, err := waid.ParsePortalID(portal.ID)
|
||||
if err == nil && wa.IsOwnJID(jid) {
|
||||
return whatsappNoteToSelfCaps
|
||||
}
|
||||
return whatsappDMCaps
|
||||
}
|
||||
return whatsappCaps
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,7 +10,6 @@ import (
|
|||
"go.mau.fi/util/jsontime"
|
||||
"go.mau.fi/util/ptr"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/appstate"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"go.mau.fi/whatsmeow/types/events"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
|
|
@ -27,68 +26,64 @@ func (wa *WhatsAppClient) GetChatInfo(ctx context.Context, portal *bridgev2.Port
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wa.getChatInfo(ctx, portalJID, nil, portal.MXID == "")
|
||||
return wa.getChatInfo(ctx, portalJID, nil)
|
||||
}
|
||||
|
||||
var ErrBroadcastList = errors.New("broadcast list bridging is currently not supported")
|
||||
var ErrNewsletterNotFound = errors.New("newsletter doesn't exist")
|
||||
|
||||
func (wa *WhatsAppClient) getChatInfo(ctx context.Context, portalJID types.JID, conv *wadb.Conversation, isNew bool) (wrapped *bridgev2.ChatInfo, err error) {
|
||||
func (wa *WhatsAppClient) getChatInfo(ctx context.Context, portalJID types.JID, conv *wadb.Conversation) (wrapped *bridgev2.ChatInfo, err error) {
|
||||
switch portalJID.Server {
|
||||
case types.DefaultUserServer, types.HiddenUserServer, types.BotServer:
|
||||
wrapped = wa.wrapDMInfo(ctx, portalJID)
|
||||
if portalJID == types.MuseJID {
|
||||
go wa.requestMuseProfile(ctx)
|
||||
}
|
||||
case types.BroadcastServer:
|
||||
if portalJID == types.StatusBroadcastJID {
|
||||
wrapped = wa.wrapStatusBroadcastInfo(ctx)
|
||||
} else {
|
||||
return nil, ErrBroadcastList
|
||||
return nil, fmt.Errorf("broadcast list bridging is currently not supported")
|
||||
}
|
||||
case types.GroupServer:
|
||||
info, err := wa.Client.GetGroupInfo(ctx, portalJID)
|
||||
info, err := wa.Client.GetGroupInfo(portalJID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wrapped = wa.wrapGroupInfo(ctx, info)
|
||||
wrapped.ExtraUpdates = bridgev2.MergeExtraUpdaters(wrapped.ExtraUpdates, updatePortalLastSyncAt)
|
||||
case types.NewsletterServer:
|
||||
info, err := wa.Client.GetNewsletterInfo(ctx, portalJID)
|
||||
info, err := wa.Client.GetNewsletterInfo(portalJID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if info == nil || info.ID.IsEmpty() {
|
||||
return nil, ErrNewsletterNotFound
|
||||
}
|
||||
wrapped = wa.wrapNewsletterInfo(ctx, info)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported server %s", portalJID.Server)
|
||||
}
|
||||
wa.addExtrasToWrapped(ctx, portalJID, wrapped, conv, isNew)
|
||||
wa.addExtrasToWrapped(ctx, portalJID, wrapped, conv)
|
||||
return wrapped, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) addExtrasToWrapped(ctx context.Context, portalJID types.JID, wrapped *bridgev2.ChatInfo, conv *wadb.Conversation, isNew bool) {
|
||||
if isNew {
|
||||
if conv == nil {
|
||||
var err error
|
||||
conv, err = wa.Main.DB.Conversation.Get(ctx, wa.UserLogin.ID, portalJID)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get history sync conversation info")
|
||||
}
|
||||
}
|
||||
if conv != nil {
|
||||
wa.applyHistoryInfo(wrapped, conv)
|
||||
func (wa *WhatsAppClient) addExtrasToWrapped(ctx context.Context, portalJID types.JID, wrapped *bridgev2.ChatInfo, conv *wadb.Conversation) {
|
||||
_, capVer := wa.Main.GetBridgeInfoVersion()
|
||||
wrapped.ExtraUpdates = bridgev2.MergeExtraUpdaters(wrapped.ExtraUpdates, updatePortalSyncMeta(capVer))
|
||||
|
||||
if conv == nil {
|
||||
var err error
|
||||
conv, err = wa.Main.DB.Conversation.Get(ctx, wa.UserLogin.ID, portalJID)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get history sync conversation info")
|
||||
}
|
||||
}
|
||||
if conv != nil {
|
||||
wa.applyHistoryInfo(wrapped, conv)
|
||||
}
|
||||
wa.applyChatSettings(ctx, portalJID, wrapped)
|
||||
}
|
||||
|
||||
func updatePortalLastSyncAt(_ context.Context, portal *bridgev2.Portal) bool {
|
||||
meta := portal.Metadata.(*waid.PortalMetadata)
|
||||
forceSave := ResyncMinInterval < 24*time.Hour || time.Since(meta.LastSync.Time) > 24*time.Hour
|
||||
meta.LastSync = jsontime.UnixNow()
|
||||
return forceSave
|
||||
func updatePortalSyncMeta(expectedCapVer int) bridgev2.ExtraUpdater[*bridgev2.Portal] {
|
||||
return func(_ context.Context, portal *bridgev2.Portal) bool {
|
||||
meta := portal.Metadata.(*waid.PortalMetadata)
|
||||
meta.LastSync = jsontime.UnixNow()
|
||||
if meta.BridgeCapsVersion != expectedCapVer {
|
||||
meta.BridgeCapsVersion = expectedCapVer
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
func updateDisappearingTimerSetAt(ts int64) bridgev2.ExtraUpdater[*bridgev2.Portal] {
|
||||
|
|
@ -108,28 +103,9 @@ 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),
|
||||
}
|
||||
wa.appStateRecoveryLock.Lock()
|
||||
_, muteStateBroken := wa.UserLogin.Metadata.(*waid.UserLoginMetadata).AppStateRecoveryAttempted[appstate.WAPatchRegularHigh]
|
||||
wa.appStateRecoveryLock.Unlock()
|
||||
userLocal := &bridgev2.UserLocalPortalInfo{}
|
||||
if !muteStateBroken {
|
||||
userLocal.MutedUntil = ptr.Ptr(chat.MutedUntil)
|
||||
} else if info.UserLocal != nil {
|
||||
userLocal.MutedUntil = info.UserLocal.MutedUntil
|
||||
}
|
||||
info.UserLocal = userLocal
|
||||
if chat.Pinned {
|
||||
info.UserLocal.Tag = ptr.Ptr(wa.Main.Config.PinnedTag)
|
||||
} else if chat.Archived {
|
||||
|
|
@ -169,20 +145,15 @@ const PrivateChatTopic = "WhatsApp private chat"
|
|||
const BotChatTopic = "WhatsApp chat with a bot"
|
||||
|
||||
func (wa *WhatsAppClient) wrapDMInfo(ctx context.Context, jid types.JID) *bridgev2.ChatInfo {
|
||||
ownID := wa.JID
|
||||
if jid.Server == types.HiddenUserServer {
|
||||
ownID = wa.GetLID()
|
||||
}
|
||||
info := &bridgev2.ChatInfo{
|
||||
Type: ptr.Ptr(database.RoomTypeDM),
|
||||
Topic: ptr.Ptr(PrivateChatTopic),
|
||||
Members: &bridgev2.ChatMemberList{
|
||||
IsFull: true,
|
||||
TotalMemberCount: 2,
|
||||
OtherUserID: waid.MakeUserID(jid),
|
||||
MemberMap: map[networkid.UserID]bridgev2.ChatMember{
|
||||
waid.MakeUserID(jid): {EventSender: wa.makeEventSender(ctx, jid)},
|
||||
waid.MakeUserID(ownID): {EventSender: wa.makeEventSender(ctx, ownID)},
|
||||
waid.MakeUserID(jid): {EventSender: wa.makeEventSender(ctx, jid)},
|
||||
waid.MakeUserID(wa.JID): {EventSender: wa.makeEventSender(ctx, wa.JID)},
|
||||
},
|
||||
PowerLevels: &bridgev2.PowerLevelOverrides{
|
||||
Events: map[event.Type]int{
|
||||
|
|
@ -192,19 +163,13 @@ func (wa *WhatsAppClient) wrapDMInfo(ctx context.Context, jid types.JID) *bridge
|
|||
event.StateBeeperDisappearingTimer: 0,
|
||||
},
|
||||
},
|
||||
ExcludeChangesFromTimeline: true,
|
||||
},
|
||||
ExcludeChangesFromTimeline: true,
|
||||
}
|
||||
var err error
|
||||
info.UserBlocked, err = wa.isUserBlocked(ctx, jid)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get blocked state")
|
||||
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)}},
|
||||
|
|
@ -228,7 +193,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),
|
||||
|
|
@ -279,8 +244,6 @@ func setTopicID(id, topic string) bridgev2.ExtraUpdater[*bridgev2.Portal] {
|
|||
|
||||
func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupInfo) *bridgev2.ChatInfo {
|
||||
sendEventPL := defaultPL
|
||||
// Community announcement groups allow non-admins to send threaded replies.
|
||||
// Power levels can't represent that, but the power level is checked separately when handling messages.
|
||||
if info.IsAnnounce && !info.IsDefaultSubGroup {
|
||||
sendEventPL = adminPL
|
||||
}
|
||||
|
|
@ -294,12 +257,11 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
|
|||
setAddressingMode(info.AddressingMode),
|
||||
setTopicID(info.TopicID, info.Topic),
|
||||
)
|
||||
syncAllMembers := wa.Main.Config.MaxMemberSync < 0 || len(info.Participants) < wa.Main.Config.MaxMemberSync
|
||||
wrapped := &bridgev2.ChatInfo{
|
||||
Name: ptr.Ptr(info.Name),
|
||||
Topic: ptr.Ptr(info.Topic),
|
||||
Members: &bridgev2.ChatMemberList{
|
||||
IsFull: !info.IsIncognito && !info.IsParent && syncAllMembers,
|
||||
IsFull: !info.IsIncognito,
|
||||
TotalMemberCount: len(info.Participants),
|
||||
MemberMap: make(map[networkid.UserID]bridgev2.ChatMember, len(info.Participants)),
|
||||
PowerLevels: &bridgev2.PowerLevelOverrides{
|
||||
|
|
@ -308,61 +270,50 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
|
|||
Ban: ptr.Ptr(nobodyPL),
|
||||
// TODO allow invites if bridge config says to allow them, or maybe if relay mode is enabled?
|
||||
Events: map[event.Type]int{
|
||||
event.StateRoomName: metaChangePL,
|
||||
event.StateRoomAvatar: metaChangePL,
|
||||
event.StateTopic: metaChangePL,
|
||||
event.EventEncrypted: defaultPL, // Need to always allow encrypted events for poll responses
|
||||
event.EventReaction: defaultPL,
|
||||
event.EventRedaction: defaultPL,
|
||||
event.EventUnstablePollResponse: defaultPL,
|
||||
event.StateRoomName: metaChangePL,
|
||||
event.StateRoomAvatar: metaChangePL,
|
||||
event.StateTopic: metaChangePL,
|
||||
event.EventReaction: defaultPL,
|
||||
event.EventRedaction: defaultPL,
|
||||
|
||||
event.StateBeeperDisappearingTimer: metaChangePL,
|
||||
// TODO always allow poll responses
|
||||
},
|
||||
},
|
||||
},
|
||||
ExcludeChangesFromTimeline: true,
|
||||
Disappear: &database.DisappearingSetting{
|
||||
Type: event.DisappearingTypeAfterSend,
|
||||
Timer: time.Duration(info.DisappearingTimer) * time.Second,
|
||||
},
|
||||
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 {
|
||||
member.PowerLevel = ptr.Ptr(adminPL)
|
||||
} else {
|
||||
if !syncAllMembers && !member.EventSender.IsFromMe {
|
||||
continue
|
||||
}
|
||||
member.PowerLevel = ptr.Ptr(defaultPL)
|
||||
}
|
||||
member.MemberEventExtra = map[string]any{
|
||||
"com.beeper.exclude_from_timeline": true,
|
||||
}
|
||||
wrapped.Members.MemberMap.Set(member)
|
||||
wrapped.Members.MemberMap[waid.MakeUserID(pcp.JID)] = member
|
||||
if pcp.JID.Server == types.HiddenUserServer && !pcp.PhoneNumber.IsEmpty() {
|
||||
wrapped.Members.MemberMap.Add(bridgev2.ChatMember{
|
||||
wrapped.Members.MemberMap[waid.MakeUserID(pcp.PhoneNumber)] = bridgev2.ChatMember{
|
||||
EventSender: bridgev2.EventSender{Sender: waid.MakeUserID(pcp.PhoneNumber)},
|
||||
Membership: event.MembershipLeave,
|
||||
PrevMembership: event.MembershipJoin,
|
||||
MemberEventExtra: map[string]any{
|
||||
"com.beeper.exclude_from_timeline": true,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if info.IsParent && !hasSelf && info.AddressingMode == types.AddressingModeLID {
|
||||
wrapped.Members.MemberMap.Add(bridgev2.ChatMember{EventSender: wa.makeEventSender(ctx, wa.GetLID())})
|
||||
}
|
||||
|
||||
if !info.LinkedParentJID.IsEmpty() {
|
||||
wrapped.ParentID = ptr.Ptr(waid.MakePortalID(info.LinkedParentJID))
|
||||
|
|
@ -473,7 +424,7 @@ func (wa *WhatsAppClient) makePortalAvatarFetcher(avatarID string, sender types.
|
|||
existingID = ""
|
||||
}
|
||||
var wrappedAvatar *bridgev2.Avatar
|
||||
avatar, err := wa.Client.GetProfilePictureInfo(ctx, jid, &whatsmeow.GetProfilePictureParams{
|
||||
avatar, err := wa.Client.GetProfilePictureInfo(jid, &whatsmeow.GetProfilePictureParams{
|
||||
ExistingID: existingID,
|
||||
IsCommunity: portal.RoomType == database.RoomTypeSpace,
|
||||
})
|
||||
|
|
@ -502,7 +453,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, "", "")
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
@ -515,7 +466,7 @@ func (wa *WhatsAppClient) makePortalAvatarFetcher(avatarID string, sender types.
|
|||
return false
|
||||
}
|
||||
//lint:ignore SA1019 TODO invent a cleaner way to fetch avatar metadata before updating?
|
||||
return portal.Internal().UpdateAvatar(ctx, wrappedAvatar, senderIntent, ts, false)
|
||||
return portal.Internal().UpdateAvatar(ctx, wrappedAvatar, senderIntent, ts)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -541,18 +492,18 @@ 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)
|
||||
avatar.Get = func(ctx context.Context) ([]byte, error) {
|
||||
meta, err := wa.Client.GetNewsletterInfo(ctx, info.ID)
|
||||
meta, err := wa.Client.GetNewsletterInfo(info.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch full res avatar info: %w", err)
|
||||
} 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"
|
||||
|
|
@ -568,8 +519,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,
|
||||
},
|
||||
},
|
||||
|
|
@ -583,27 +534,10 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne
|
|||
event.StateTopic: adminPL,
|
||||
event.EventReaction: defaultPL,
|
||||
event.EventRedaction: defaultPL,
|
||||
// TODO support poll votes in channels
|
||||
//event.EventUnstablePollResponse: defaultPL,
|
||||
// TODO always allow poll responses
|
||||
},
|
||||
},
|
||||
},
|
||||
Type: ptr.Ptr(database.RoomTypeDefault),
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) isUserBlocked(ctx context.Context, jid types.JID) (*bool, error) {
|
||||
wa.blocklistLock.Lock()
|
||||
defer wa.blocklistLock.Unlock()
|
||||
meta := wa.UserLogin.Metadata.(*waid.UserLoginMetadata)
|
||||
blocklist := meta.Blocklist
|
||||
if blocklist == nil {
|
||||
runHandlers := wa.resyncBlocklistUnlocked(ctx)
|
||||
go runHandlers()
|
||||
blocklist = meta.Blocklist
|
||||
}
|
||||
if blocklist == nil {
|
||||
return nil, nil
|
||||
}
|
||||
return new(blocklist.IsBlocked(jid)), nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,13 +22,12 @@ import (
|
|||
"fmt"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/exsync"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/appstate"
|
||||
waBinary "go.mau.fi/whatsmeow/binary"
|
||||
"go.mau.fi/whatsmeow/proto/waHistorySync"
|
||||
"go.mau.fi/whatsmeow/proto/waWa6"
|
||||
"go.mau.fi/whatsmeow/store"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
|
|
@ -38,7 +37,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"
|
||||
)
|
||||
|
|
@ -47,19 +45,14 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2.
|
|||
w := &WhatsAppClient{
|
||||
Main: wa,
|
||||
UserLogin: login,
|
||||
MC: noopMCInstance,
|
||||
|
||||
historySyncWakeup: make(chan struct{}, 1),
|
||||
resyncQueue: make(map[types.JID]resyncQueueItem),
|
||||
directMediaRetries: make(map[networkid.MessageID]*directMediaRetry),
|
||||
mediaRetryLock: semaphore.NewWeighted(wa.Config.HistorySync.MediaRequests.MaxAsyncHandle),
|
||||
pushNamesSynced: exsync.NewEvent(),
|
||||
appStateFullSyncAttempted: make(map[appstate.WAPatchName]time.Time),
|
||||
previousOwnLIDs: exsync.NewSetWithItems(login.Metadata.(*waid.UserLoginMetadata).PreviousOwnLIDs),
|
||||
|
||||
onDemandHistorySyncRequests: make(map[types.JID]onDemandHistorySyncRequest),
|
||||
|
||||
disableNewsletter: store.BaseClientPayload.GetUserAgent().GetPlatform() == waWa6.ClientPayload_UserAgent_MACOS,
|
||||
historySyncs: make(chan *waHistorySync.HistorySync, 64),
|
||||
historySyncWakeup: make(chan struct{}, 1),
|
||||
resyncQueueCh: make(chan resyncQueueItem, 256),
|
||||
directMediaRetries: make(map[networkid.MessageID]*directMediaRetry),
|
||||
mediaRetryLock: semaphore.NewWeighted(wa.Config.HistorySync.MediaRequests.MaxAsyncHandle),
|
||||
pushNamesSynced: exsync.NewEvent(),
|
||||
createDedup: exsync.NewSet[types.MessageID](),
|
||||
}
|
||||
login.Client = w
|
||||
|
||||
|
|
@ -74,24 +67,23 @@ 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()
|
||||
w.Client = whatsmeow.NewClient(w.Device, waLog.Zerolog(log))
|
||||
w.Client.AddEventHandlerWithSuccessStatus(w.handleWAEvent)
|
||||
w.Client.SynchronousAck = true
|
||||
w.Client.EnableDecryptedEventBuffer = wa.Bridge.Config.PortalEventBuffer == 0
|
||||
w.Client.ManualHistorySyncDownload = true
|
||||
if bridgev2.PortalEventBuffer == 0 {
|
||||
w.Client.SynchronousAck = true
|
||||
w.Client.EnableDecryptedEventBuffer = true
|
||||
w.Client.ManualHistorySyncDownload = true
|
||||
}
|
||||
w.Client.SendReportingTokens = true
|
||||
w.Client.AutomaticMessageRerequestFromPhone = true
|
||||
w.Client.UnifiedSessionInterval = 24 * time.Hour
|
||||
w.Client.GetMessageForRetry = w.trackNotFoundRetry
|
||||
w.Client.PreRetryCallback = w.trackFoundRetry
|
||||
w.Client.BackgroundEventCtx = w.UserLogin.Log.WithContext(wa.Bridge.BackgroundCtx)
|
||||
w.Client.SetForceActiveDeliveryReceipts(wa.Config.ForceActiveDeliveryReceipts)
|
||||
w.Client.InitialAutoReconnect = wa.Config.InitialAutoReconnect
|
||||
w.Client.UseRetryMessageStore = wa.Config.UseWhatsAppRetryStore
|
||||
} else {
|
||||
w.UserLogin.Log.Warn().Stringer("jid", w.JID).Msg("No device found for user in whatsmeow store")
|
||||
}
|
||||
|
|
@ -110,38 +102,20 @@ type WhatsAppClient struct {
|
|||
Client *whatsmeow.Client
|
||||
Device *store.Device
|
||||
JID types.JID
|
||||
LID types.JID
|
||||
MC mClient
|
||||
|
||||
historySyncs chan *waHistorySync.HistorySync
|
||||
historySyncWakeup chan struct{}
|
||||
stopLoops atomic.Pointer[context.CancelFunc]
|
||||
resyncQueue map[types.JID]resyncQueueItem
|
||||
resyncQueueCh chan resyncQueueItem
|
||||
resyncQueueLock sync.Mutex
|
||||
nextResync time.Time
|
||||
directMediaRetries map[networkid.MessageID]*directMediaRetry
|
||||
directMediaLock sync.Mutex
|
||||
avatarLock exsync.KeyedMutex[types.JID]
|
||||
mediaRetryLock *semaphore.Weighted
|
||||
offlineSyncWaiter atomic.Pointer[chan error]
|
||||
isNewLogin bool
|
||||
pushNamesSynced *exsync.Event
|
||||
lastPresence types.Presence
|
||||
blocklistLock sync.Mutex
|
||||
|
||||
disableNewsletter bool
|
||||
|
||||
appStateRecoveryLock sync.Mutex
|
||||
appStateFullSyncAttempted map[appstate.WAPatchName]time.Time
|
||||
|
||||
wasaResyncLock sync.Mutex
|
||||
museProfileLock sync.Mutex
|
||||
lastMuseProfileRequest time.Time
|
||||
|
||||
onDemandHistorySyncLock sync.Mutex
|
||||
onDemandHistorySyncRequests map[types.JID]onDemandHistorySyncRequest
|
||||
|
||||
previousOwnLIDs *exsync.Set[types.JID]
|
||||
previousOwnLIDsLock sync.Mutex
|
||||
createDedup *exsync.Set[types.MessageID]
|
||||
}
|
||||
|
||||
var (
|
||||
|
|
@ -149,7 +123,6 @@ var (
|
|||
_ bridgev2.PushableNetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.BackgroundSyncingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.ChatViewingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.StickerImportingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
)
|
||||
|
||||
var pushCfg = &bridgev2.PushConfig{
|
||||
|
|
@ -205,67 +178,25 @@ 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) ||
|
||||
wa.previousOwnLIDs.Has(jid.ToNonAD())
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) addPreviousOwnLID(ctx context.Context, jid types.JID) {
|
||||
if jid.Server != types.HiddenUserServer || wa.IsOwnJID(jid) {
|
||||
return
|
||||
}
|
||||
jid = jid.ToNonAD()
|
||||
wa.previousOwnLIDsLock.Lock()
|
||||
defer wa.previousOwnLIDsLock.Unlock()
|
||||
if !wa.previousOwnLIDs.Add(jid) {
|
||||
return
|
||||
}
|
||||
zerolog.Ctx(ctx).Info().Stringer("lid", jid).Msg("Found previous own LID in history sync")
|
||||
wa.UserLogin.Metadata.(*waid.UserLoginMetadata).PreviousOwnLIDs = wa.previousOwnLIDs.AsList()
|
||||
err := wa.UserLogin.Save(ctx)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to save user login after adding previous own LID")
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) GetLID() types.JID {
|
||||
if wa.LID.IsEmpty() && !wa.JID.IsEmpty() {
|
||||
wa.LID = wa.GetStore().GetLID()
|
||||
}
|
||||
return wa.LID
|
||||
return userID == waid.MakeUserID(wa.JID)
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) Connect(ctx context.Context) {
|
||||
wa.connect(ctx)
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) connect(ctx context.Context) bool {
|
||||
if wa.Client == nil {
|
||||
state := status.BridgeState{
|
||||
StateEvent: status.StateBadCredentials,
|
||||
Error: WANotLoggedIn,
|
||||
}
|
||||
wa.UserLogin.BridgeState.Send(state)
|
||||
return false
|
||||
return
|
||||
}
|
||||
wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnecting})
|
||||
wa.Main.firstClientConnectOnce.Do(wa.Main.onFirstClientConnect)
|
||||
if err := wa.Main.updateProxy(ctx, wa.Client, false); err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to update proxy")
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return false
|
||||
}
|
||||
wa.initMC()
|
||||
wa.startLoops()
|
||||
wa.Client.BackgroundEventCtx = wa.UserLogin.Log.WithContext(wa.Main.Bridge.BackgroundCtx)
|
||||
zerolog.Ctx(ctx).Debug().Msg("Connecting to WhatsApp")
|
||||
if err := wa.Client.ConnectContext(ctx); err != nil {
|
||||
wa.callStopLoops()
|
||||
wa.Client.BackgroundEventCtx = wa.Main.Bridge.BackgroundCtx
|
||||
if err := wa.Client.Connect(); err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to connect to WhatsApp")
|
||||
state := status.BridgeState{
|
||||
StateEvent: status.StateUnknownError,
|
||||
|
|
@ -275,9 +206,7 @@ func (wa *WhatsAppClient) connect(ctx context.Context) bool {
|
|||
},
|
||||
}
|
||||
wa.UserLogin.BridgeState.Send(state)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) notifyOfflineSyncWaiter(err error) {
|
||||
|
|
@ -323,11 +252,9 @@ func (wa *WhatsAppClient) ConnectBackground(ctx context.Context, params *bridgev
|
|||
return payload
|
||||
}
|
||||
defer func() {
|
||||
if cli := wa.Client; cli != nil {
|
||||
cli.GetClientPayload = nil
|
||||
}
|
||||
wa.Client.GetClientPayload = nil
|
||||
}()
|
||||
err := wa.Client.ConnectContext(ctx)
|
||||
err := wa.Client.Connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -352,7 +279,7 @@ func (wa *WhatsAppClient) ConnectBackground(ctx context.Context, params *bridgev
|
|||
|
||||
func (wa *WhatsAppClient) sendPNData(ctx context.Context, pn string) error {
|
||||
//lint:ignore SA1019 this is supposed to be dangerous
|
||||
resp, err := wa.Client.DangerousInternals().SendIQ(ctx, whatsmeow.DangerousInfoQuery{
|
||||
resp, err := wa.Client.DangerousInternals().SendIQ(whatsmeow.DangerousInfoQuery{
|
||||
Namespace: "urn:xmpp:whatsapp:push",
|
||||
Type: "get",
|
||||
To: types.ServerJID,
|
||||
|
|
@ -360,6 +287,7 @@ func (wa *WhatsAppClient) sendPNData(ctx context.Context, pn string) error {
|
|||
Tag: "pn",
|
||||
Content: pn,
|
||||
}},
|
||||
Context: ctx,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send pn: %w", err)
|
||||
|
|
@ -374,7 +302,7 @@ func (wa *WhatsAppClient) sendPNData(ctx context.Context, pn string) error {
|
|||
}
|
||||
zerolog.Ctx(ctx).Debug().Str("cat_data", string(catContentBytes)).Msg("Received cat response from sending pn data")
|
||||
//lint:ignore SA1019 this is supposed to be dangerous
|
||||
err = wa.Client.DangerousInternals().SendNode(ctx, waBinary.Node{
|
||||
err = wa.Client.DangerousInternals().SendNode(waBinary.Node{
|
||||
Tag: "ib",
|
||||
Content: []waBinary.Node{{
|
||||
Tag: "cat",
|
||||
|
|
@ -389,14 +317,13 @@ func (wa *WhatsAppClient) sendPNData(ctx context.Context, pn string) error {
|
|||
}
|
||||
|
||||
func (wa *WhatsAppClient) startLoops() {
|
||||
ctx, cancel := context.WithCancel(wa.Main.Bridge.BackgroundCtx)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
oldStop := wa.stopLoops.Swap(&cancel)
|
||||
if oldStop != nil {
|
||||
(*oldStop)()
|
||||
}
|
||||
ctx = wa.UserLogin.Log.WithContext(ctx)
|
||||
go wa.historySyncLoop(ctx)
|
||||
go wa.ghostResyncLoop(ctx)
|
||||
go wa.resyncLoop(ctx)
|
||||
if mrc := wa.Main.Config.HistorySync.MediaRequests; mrc.AutoRequestMedia && mrc.RequestMethod == MediaRequestMethodLocalTime {
|
||||
go wa.mediaRequestLoop(ctx)
|
||||
}
|
||||
|
|
@ -412,14 +339,10 @@ func (wa *WhatsAppClient) GetStore() *store.Device {
|
|||
return store.NoopDevice
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) callStopLoops() {
|
||||
func (wa *WhatsAppClient) Disconnect() {
|
||||
if stopHistorySyncLoop := wa.stopLoops.Swap(nil); stopHistorySyncLoop != nil {
|
||||
(*stopHistorySyncLoop)()
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) Disconnect() {
|
||||
wa.callStopLoops()
|
||||
if cli := wa.Client; cli != nil {
|
||||
cli.Disconnect()
|
||||
}
|
||||
|
|
@ -434,10 +357,6 @@ func (wa *WhatsAppClient) LogoutRemote(ctx context.Context) {
|
|||
}
|
||||
wa.Disconnect()
|
||||
wa.Client = nil
|
||||
err := wa.Main.DB.Conversation.DeleteAll(ctx, wa.UserLogin.ID)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to delete history sync data on logout")
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) IsLoggedIn() bool {
|
||||
|
|
@ -488,25 +407,18 @@ func (wa *WhatsAppClient) HandleMatrixViewingChat(ctx context.Context, msg *brid
|
|||
}
|
||||
|
||||
if wa.lastPresence != presence {
|
||||
err := wa.updatePresence(ctx, presence)
|
||||
err := wa.updatePresence(presence)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to set presence when viewing chat")
|
||||
}
|
||||
}
|
||||
|
||||
if msg.Portal != nil && msg.Portal.ID == waid.MakePortalID(types.MuseJID) {
|
||||
go wa.requestMuseProfile(ctx)
|
||||
}
|
||||
|
||||
if msg.Portal == nil || msg.Portal.Metadata.(*waid.PortalMetadata).LastSync.Add(5*time.Minute).After(time.Now()) {
|
||||
// If we resynced this portal within the last 5 minutes, don't do it again
|
||||
if msg.Portal == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reset, but don't save, portal last sync time for immediate sync now
|
||||
msg.Portal.Metadata.(*waid.PortalMetadata).LastSync.Time = time.Time{}
|
||||
// Enqueue for the sync, don't block on it completing
|
||||
wa.EnqueuePortalResync(msg.Portal, true)
|
||||
// Ask the queue to resync if needed (old or version mismatch). No-op otherwise.
|
||||
wa.EnqueuePortalResync(msg.Portal)
|
||||
|
||||
if msg.Portal.OtherUserID != "" {
|
||||
// If this is a DM, also sync the ghost of the other user immediately
|
||||
|
|
@ -518,8 +430,6 @@ func (wa *WhatsAppClient) HandleMatrixViewingChat(ctx context.Context, msg *brid
|
|||
Str("other_user_id", string(msg.Portal.OtherUserID)).
|
||||
Msg("No ghost found for other user in portal")
|
||||
} else {
|
||||
// Reset, but don't save, portal last sync time for immediate sync now
|
||||
ghost.Metadata.(*waid.GhostMetadata).LastSync.Time = time.Time{}
|
||||
wa.EnqueueGhostResync(ghost)
|
||||
}
|
||||
}
|
||||
|
|
@ -527,19 +437,10 @@ func (wa *WhatsAppClient) HandleMatrixViewingChat(ctx context.Context, msg *brid
|
|||
return nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) updatePresence(ctx context.Context, presence types.Presence) error {
|
||||
err := wa.Client.SendPresence(ctx, presence)
|
||||
func (wa *WhatsAppClient) updatePresence(presence types.Presence) error {
|
||||
err := wa.Client.SendPresence(presence)
|
||||
if err == nil {
|
||||
wa.lastPresence = presence
|
||||
}
|
||||
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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,15 +17,12 @@
|
|||
package connector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/exslices"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/appstate"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
|
|
@ -68,7 +65,7 @@ func fnAccept(ce *commands.Event) {
|
|||
ce.Reply("Login not found")
|
||||
} else if !login.Client.IsLoggedIn() {
|
||||
ce.Reply("Not logged in")
|
||||
} else if err = login.Client.(*WhatsAppClient).Client.JoinGroupWithInvite(ce.Ctx, meta.JID, meta.Inviter, meta.Code, meta.Expiration); err != nil {
|
||||
} else if err = login.Client.(*WhatsAppClient).Client.JoinGroupWithInvite(meta.JID, meta.Inviter, meta.Code, meta.Expiration); err != nil {
|
||||
ce.Log.Err(err).Msg("Failed to accept group invite")
|
||||
ce.Reply("Failed to accept group invite: %v", err)
|
||||
} else {
|
||||
|
|
@ -122,7 +119,11 @@ func fnSync(ce *commands.Event) {
|
|||
ce.Reply("Failed to get joined groups: %v", err)
|
||||
return
|
||||
}
|
||||
_, capVer := wa.Main.GetBridgeInfoVersion()
|
||||
for _, group := range groups {
|
||||
wrapped := wa.wrapGroupInfo(ce.Ctx, group)
|
||||
wrapped.ExtraUpdates = bridgev2.MergeExtraUpdaters(wrapped.ExtraUpdates, updatePortalSyncMeta(capVer))
|
||||
wa.addExtrasToWrapped(ce.Ctx, group.JID, wrapped, nil)
|
||||
login.QueueRemoteEvent(&simplevent.ChatResync{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventChatResync,
|
||||
|
|
@ -130,12 +131,7 @@ func fnSync(ce *commands.Event) {
|
|||
LogContext: logContext,
|
||||
CreatePortal: true,
|
||||
},
|
||||
GetChatInfoFunc: func(ctx context.Context, portal *bridgev2.Portal) (*bridgev2.ChatInfo, error) {
|
||||
wrapped := wa.wrapGroupInfo(ce.Ctx, group)
|
||||
wrapped.ExtraUpdates = bridgev2.MergeExtraUpdaters(wrapped.ExtraUpdates, updatePortalLastSyncAt)
|
||||
wa.addExtrasToWrapped(ce.Ctx, group.JID, wrapped, nil, portal.MXID == "")
|
||||
return wrapped, nil
|
||||
},
|
||||
ChatInfo: wrapped,
|
||||
})
|
||||
}
|
||||
ce.Reply("Queued syncs for %d groups", len(groups))
|
||||
|
|
@ -146,11 +142,7 @@ func fnSync(ce *commands.Event) {
|
|||
wa.resyncContacts(true, false)
|
||||
ce.React("✅")
|
||||
case "appstate":
|
||||
names := appstate.AllPatchNames[:]
|
||||
if len(ce.Args) > 1 {
|
||||
names = parseAppStateNames(ce, ce.Args[1:])
|
||||
}
|
||||
for _, name := range names {
|
||||
for _, name := range appstate.AllPatchNames {
|
||||
err := wa.Client.FetchAppState(ce.Ctx, name, true, false)
|
||||
if errors.Is(err, appstate.ErrKeyNotFound) {
|
||||
ce.Reply("Key not found error syncing app state %s: %v\n\nKey requests are sent automatically, and the sync should happen in the background after your phone responds.", name, err)
|
||||
|
|
@ -168,50 +160,6 @@ func fnSync(ce *commands.Event) {
|
|||
}
|
||||
}
|
||||
|
||||
func parseAppStateNames(ce *commands.Event, args []string) []appstate.WAPatchName {
|
||||
return exslices.CastFuncFilter(args, func(name string) (appstate.WAPatchName, bool) {
|
||||
if !slices.Contains(appstate.AllPatchNames[:], appstate.WAPatchName(name)) {
|
||||
ce.Reply("Invalid app state name `%s`", name)
|
||||
return "", false
|
||||
}
|
||||
return appstate.WAPatchName(name), true
|
||||
})
|
||||
}
|
||||
|
||||
var cmdResetAppState = &commands.FullHandler{
|
||||
Func: fnResetAppState,
|
||||
Name: "reset-appstate",
|
||||
Help: commands.HelpMeta{
|
||||
Section: commands.HelpSectionAdmin,
|
||||
Description: "Ask your phone to reset broken app state. This logs out all linked devices.",
|
||||
Args: "<names...>",
|
||||
},
|
||||
RequiresLogin: true,
|
||||
}
|
||||
|
||||
func fnResetAppState(ce *commands.Event) {
|
||||
login := ce.User.GetDefaultLogin()
|
||||
if login == nil {
|
||||
ce.Reply("Login not found")
|
||||
return
|
||||
}
|
||||
if len(ce.Args) == 0 {
|
||||
ce.Reply("Usage: `$cmdprefix reset-appstate <names...>`\n\nThis logs out all linked devices, including the bridge. You'll have to log in again afterwards.")
|
||||
return
|
||||
}
|
||||
names := parseAppStateNames(ce, ce.Args)
|
||||
if len(names) != len(ce.Args) {
|
||||
return
|
||||
}
|
||||
wa := login.Client.(*WhatsAppClient)
|
||||
_, err := wa.Client.SendPeerMessage(ce.Ctx, whatsmeow.BuildFatalAppStateExceptionNotification(names...))
|
||||
if err != nil {
|
||||
ce.Reply("Failed to send app state reset request: %v", err)
|
||||
return
|
||||
}
|
||||
ce.Reply("Sent app state reset request. Your phone should log out all linked devices shortly, after which you can log in again.")
|
||||
}
|
||||
|
||||
var cmdInviteLink = &commands.FullHandler{
|
||||
Func: fnInviteLink,
|
||||
Name: "invite-link",
|
||||
|
|
@ -242,7 +190,7 @@ func fnInviteLink(ce *commands.Event) {
|
|||
ce.Reply("Can't get invite link to private chat")
|
||||
} else if portalJID.IsBroadcastList() {
|
||||
ce.Reply("Can't get invite link to broadcast list")
|
||||
} else if link, err := wa.Client.GetGroupInviteLink(ce.Ctx, portalJID, reset); err != nil {
|
||||
} else if link, err := wa.Client.GetGroupInviteLink(portalJID, reset); err != nil {
|
||||
ce.Reply("Failed to get invite link: %v", err)
|
||||
} else {
|
||||
ce.Reply(link)
|
||||
|
|
@ -272,14 +220,14 @@ func fnResolveLink(ce *commands.Event) {
|
|||
}
|
||||
wa := login.Client.(*WhatsAppClient)
|
||||
if strings.HasPrefix(ce.Args[0], whatsmeow.InviteLinkPrefix) {
|
||||
group, err := wa.Client.GetGroupInfoFromLink(ce.Ctx, ce.Args[0])
|
||||
group, err := wa.Client.GetGroupInfoFromLink(ce.Args[0])
|
||||
if err != nil {
|
||||
ce.Reply("Failed to get group info: %v", err)
|
||||
return
|
||||
}
|
||||
ce.Reply("That invite link points at %s (`%s`)", group.Name, group.JID)
|
||||
} else if strings.HasPrefix(ce.Args[0], whatsmeow.BusinessMessageLinkPrefix) || strings.HasPrefix(ce.Args[0], whatsmeow.BusinessMessageLinkDirectPrefix) {
|
||||
target, err := wa.Client.ResolveBusinessMessageLink(ce.Ctx, ce.Args[0])
|
||||
target, err := wa.Client.ResolveBusinessMessageLink(ce.Args[0])
|
||||
if err != nil {
|
||||
ce.Reply("Failed to get business info: %v", err)
|
||||
return
|
||||
|
|
@ -294,7 +242,7 @@ func fnResolveLink(ce *commands.Event) {
|
|||
}
|
||||
ce.Reply("That link points at %s (+%s).%s", target.PushName, target.JID.User, message)
|
||||
} else if strings.HasPrefix(ce.Args[0], whatsmeow.ContactQRLinkPrefix) || strings.HasPrefix(ce.Args[0], whatsmeow.ContactQRLinkDirectPrefix) {
|
||||
target, err := wa.Client.ResolveContactQRLink(ce.Ctx, ce.Args[0])
|
||||
target, err := wa.Client.ResolveContactQRLink(ce.Args[0])
|
||||
if err != nil {
|
||||
ce.Reply("Failed to get contact info: %v", err)
|
||||
return
|
||||
|
|
@ -333,7 +281,7 @@ func fnJoin(ce *commands.Event) {
|
|||
wa := login.Client.(*WhatsAppClient)
|
||||
|
||||
if strings.HasPrefix(ce.Args[0], whatsmeow.InviteLinkPrefix) {
|
||||
jid, err := wa.Client.JoinGroupWithLink(ce.Ctx, ce.Args[0])
|
||||
jid, err := wa.Client.JoinGroupWithLink(ce.Args[0])
|
||||
if err != nil {
|
||||
ce.Reply("Failed to join group: %v", err)
|
||||
return
|
||||
|
|
@ -341,12 +289,12 @@ func fnJoin(ce *commands.Event) {
|
|||
ce.Log.Debug().Stringer("group_jid", jid).Msg("User successfully joined WhatsApp group with link")
|
||||
ce.Reply("Successfully joined group `%s`, the portal should be created momentarily", jid)
|
||||
} else if strings.HasPrefix(ce.Args[0], whatsmeow.NewsletterLinkPrefix) {
|
||||
info, err := wa.Client.GetNewsletterInfoWithInvite(ce.Ctx, ce.Args[0])
|
||||
info, err := wa.Client.GetNewsletterInfoWithInvite(ce.Args[0])
|
||||
if err != nil {
|
||||
ce.Reply("Failed to get channel info: %v", err)
|
||||
return
|
||||
}
|
||||
err = wa.Client.FollowNewsletter(ce.Ctx, info.ID)
|
||||
err = wa.Client.FollowNewsletter(info.ID)
|
||||
if err != nil {
|
||||
ce.Reply("Failed to follow channel: %v", err)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package connector
|
|||
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
|
@ -51,9 +50,6 @@ type Config struct {
|
|||
ForceActiveDeliveryReceipts bool `yaml:"force_active_delivery_receipts"`
|
||||
DirectMediaAutoRequest bool `yaml:"direct_media_auto_request"`
|
||||
InitialAutoReconnect bool `yaml:"initial_auto_reconnect"`
|
||||
UseWhatsAppRetryStore bool `yaml:"use_whatsapp_retry_store"`
|
||||
MaxMemberSync int `yaml:"max_member_sync"`
|
||||
LazyAvatars bool `yaml:"lazy_avatars"`
|
||||
|
||||
AnimatedSticker msgconv.AnimatedStickerConfig `yaml:"animated_sticker"`
|
||||
|
||||
|
|
@ -73,8 +69,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:"-"`
|
||||
|
|
@ -93,15 +87,7 @@ func (c *Config) UnmarshalYAML(node *yaml.Node) error {
|
|||
func (c *Config) PostProcess() error {
|
||||
var err error
|
||||
c.displaynameTemplate, err = template.New("displayname").Parse(c.DisplaynameTemplate)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// Try to execute template to make sure it's valid
|
||||
_, err = c.formatDisplayname(types.PSAJID, "", types.ContactInfo{})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to execute displayname template: %w", err)
|
||||
}
|
||||
return nil
|
||||
return err
|
||||
}
|
||||
|
||||
func upgradeConfig(helper up.Helper) {
|
||||
|
|
@ -130,9 +116,6 @@ func upgradeConfig(helper up.Helper) {
|
|||
helper.Copy(up.Bool, "force_active_delivery_receipts")
|
||||
helper.Copy(up.Bool, "direct_media_auto_request")
|
||||
helper.Copy(up.Bool, "initial_auto_reconnect")
|
||||
helper.Copy(up.Bool, "use_whatsapp_retry_store")
|
||||
helper.Copy(up.Int, "max_member_sync")
|
||||
helper.Copy(up.Bool, "lazy_avatars")
|
||||
|
||||
helper.Copy(up.Str, "animated_sticker", "target")
|
||||
helper.Copy(up.Int, "animated_sticker", "args", "width")
|
||||
|
|
@ -149,7 +132,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 {
|
||||
|
|
@ -164,7 +146,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
|
||||
|
|
@ -183,21 +165,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:]
|
||||
}
|
||||
|
|
@ -209,7 +183,6 @@ func (wa *WhatsAppConnector) GetConfig() (string, any, up.Upgrader) {
|
|||
{"proxy"},
|
||||
{"displayname_template"},
|
||||
{"call_start_notices"},
|
||||
{"animated_sticker"},
|
||||
{"history_sync"},
|
||||
},
|
||||
Base: ExampleConfig,
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue