mirror of
https://github.com/mautrix/whatsapp.git
synced 2026-11-09 23:59:17 -05:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c29d5632bb |
72 changed files with 1778 additions and 6880 deletions
|
|
@ -1 +0,0 @@
|
||||||
code_format_allowlist: ["wa-connect-failure-%d"]
|
|
||||||
18
.github/ISSUE_TEMPLATE/bug.md
vendored
18
.github/ISSUE_TEMPLATE/bug.md
vendored
|
|
@ -1,18 +1,14 @@
|
||||||
---
|
---
|
||||||
name: Bug report
|
name: Bug report
|
||||||
about: If something is definitely wrong in the bridge (rather than just a setup issue),
|
about: If something is definitely wrong in the bridge (rather than just a setup issue),
|
||||||
file a bug report. Remember to include relevant logs. Asking in the Matrix room first
|
file a bug report. Remember to include relevant logs.
|
||||||
is strongly recommended.
|
labels: bug
|
||||||
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
|
If you aren't sure what's needed, ask in the Matrix room rather than opening an
|
||||||
|
incomplete issue. Issues with insufficient detail will likely just be ignored.
|
||||||
<!-- 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: ``
|
|
||||||
|
|
|
||||||
2
.github/ISSUE_TEMPLATE/enhancement.md
vendored
2
.github/ISSUE_TEMPLATE/enhancement.md
vendored
|
|
@ -1,6 +1,6 @@
|
||||||
---
|
---
|
||||||
name: Enhancement request
|
name: Enhancement request
|
||||||
about: Submit a feature request or other suggestion
|
about: Submit a feature request or other suggestion
|
||||||
type: Feature
|
labels: enhancement
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
|
||||||
9
.github/workflows/go.yml
vendored
9
.github/workflows/go.yml
vendored
|
|
@ -11,14 +11,14 @@ jobs:
|
||||||
strategy:
|
strategy:
|
||||||
fail-fast: false
|
fail-fast: false
|
||||||
matrix:
|
matrix:
|
||||||
go-version: ["1.26", "1.27"]
|
go-version: ["1.23", "1.24"]
|
||||||
name: Lint ${{ matrix.go-version == '1.27' && '(latest)' || '(old)' }}
|
name: Lint ${{ matrix.go-version == '1.24' && '(latest)' || '(old)' }}
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v7
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v7
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: ${{ matrix.go-version }}
|
go-version: ${{ matrix.go-version }}
|
||||||
cache: true
|
cache: true
|
||||||
|
|
@ -30,7 +30,6 @@ jobs:
|
||||||
run: |
|
run: |
|
||||||
go install golang.org/x/tools/cmd/goimports@latest
|
go install golang.org/x/tools/cmd/goimports@latest
|
||||||
go install honnef.co/go/tools/cmd/staticcheck@latest
|
go install honnef.co/go/tools/cmd/staticcheck@latest
|
||||||
go install github.com/beeper/pre-commit-go/bridgelint@latest
|
|
||||||
export PATH="$HOME/go/bin:$PATH"
|
export PATH="$HOME/go/bin:$PATH"
|
||||||
|
|
||||||
- name: Run pre-commit
|
- name: Run pre-commit
|
||||||
|
|
|
||||||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
|
|
@ -17,7 +17,7 @@ jobs:
|
||||||
lock-stale:
|
lock-stale:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: dessant/lock-threads@v6
|
- uses: dessant/lock-threads@v5
|
||||||
id: lock
|
id: lock
|
||||||
with:
|
with:
|
||||||
issue-inactive-days: 90
|
issue-inactive-days: 90
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
repos:
|
repos:
|
||||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||||
rev: v6.0.0
|
rev: v5.0.0
|
||||||
hooks:
|
hooks:
|
||||||
- id: trailing-whitespace
|
- id: trailing-whitespace
|
||||||
exclude_types: [markdown]
|
exclude_types: [markdown]
|
||||||
|
|
@ -9,7 +9,7 @@ repos:
|
||||||
- id: check-added-large-files
|
- id: check-added-large-files
|
||||||
|
|
||||||
- repo: https://github.com/tekwizely/pre-commit-golang
|
- repo: https://github.com/tekwizely/pre-commit-golang
|
||||||
rev: v1.0.0-rc.4
|
rev: v1.0.0-rc.1
|
||||||
hooks:
|
hooks:
|
||||||
- id: go-imports-repo
|
- id: go-imports-repo
|
||||||
args:
|
args:
|
||||||
|
|
@ -21,12 +21,9 @@ repos:
|
||||||
- id: go-mod-tidy
|
- id: go-mod-tidy
|
||||||
|
|
||||||
- repo: https://github.com/beeper/pre-commit-go
|
- repo: https://github.com/beeper/pre-commit-go
|
||||||
rev: v0.5.0
|
rev: v0.4.2
|
||||||
hooks:
|
hooks:
|
||||||
- id: zerolog-ban-msgf
|
- id: zerolog-ban-msgf
|
||||||
- id: zerolog-use-stringer
|
- id: zerolog-use-stringer
|
||||||
- id: prevent-literal-http-methods
|
- id: prevent-literal-http-methods
|
||||||
- id: zerolog-ban-global-log
|
- id: zerolog-ban-global-log
|
||||||
- id: bridgelint
|
|
||||||
language: system
|
|
||||||
verbose: true
|
|
||||||
|
|
|
||||||
215
CHANGELOG.md
215
CHANGELOG.md
|
|
@ -1,218 +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.
|
|
||||||
Upgrading directly from versions prior to v0.11.0 is not supported.
|
|
||||||
* If you've been using the bridge since before v0.11.0 and have prevented the
|
|
||||||
bridge from writing to the config, you must either update the config
|
|
||||||
manually or allow the bridge to update it for you **before** upgrading to
|
|
||||||
this release (i.e. run v0.12.4 once with config writing allowed).
|
|
||||||
* Added support for changing group name/topic/avatar from Matrix
|
|
||||||
(thanks to [@Petersmit27] in [#834]).
|
|
||||||
* Added `RedactedPhone` placeholder for displayname templates. This allows
|
|
||||||
community announcement groups (where you can't see participants phone numbers)
|
|
||||||
to have better names than random numbers.
|
|
||||||
* Added support for `com.beeper.disappearing_timer` state event, which stores
|
|
||||||
the disappearing setting of chats and allows changing the setting from Matrix.
|
|
||||||
* Added lottieconverter to Docker images to enable converting animated stickers
|
|
||||||
from WhatsApp.
|
|
||||||
* Added support for creating WhatsApp groups.
|
|
||||||
* Fixed sent PNGs not being rendered on WhatsApp iOS.
|
|
||||||
|
|
||||||
[@Petersmit27]: https://github.com/Petersmit27
|
|
||||||
[#834]: https://github.com/mautrix/whatsapp/pull/834
|
|
||||||
|
|
||||||
# v0.12.4 (2025-08-16)
|
|
||||||
|
|
||||||
* Deprecated legacy provisioning API. The `/_matrix/provision/v1` endpoints will
|
|
||||||
be deleted in the next release.
|
|
||||||
* Bumped minimum Go version to 1.24.
|
|
||||||
* Added support for bridging HD dual uploads from WhatsApp into edits on Matrix.
|
|
||||||
* Added better placeholders for pin and keep messages from WhatsApp.
|
|
||||||
* Fixed bridging animated webp stickers to WhatsApp.
|
|
||||||
* Note that non-square stickers may appear corrupted on native clients.
|
|
||||||
The bridge will not automatically add padding to animated stickers like it
|
|
||||||
does for static ones.
|
|
||||||
* Fixed avatar changes not reflecting on both the LID and phone number ghost of
|
|
||||||
a given user in certain cases.
|
|
||||||
* Fixed first message after group LID migration still using the phone number
|
|
||||||
ghost.
|
|
||||||
* Fixed bot messages in DMs being split into another portal room.
|
|
||||||
* Fixed new group members not having a phone number name in some cases.
|
|
||||||
|
|
||||||
# v0.12.3 (2025-07-16)
|
|
||||||
|
|
||||||
* Further improved support for `@lid` users.
|
|
||||||
* Added automatic conversion when sending quicktime/mov videos to WhatsApp.
|
|
||||||
* Fixed disappearing message timer not automatically fixing itself in some cases.
|
|
||||||
* Fixed call notices being sent to DM portal even if the call was in a group.
|
|
||||||
|
|
||||||
# v0.12.2 (2025-06-16)
|
|
||||||
|
|
||||||
* Improved support for `@lid` users.
|
|
||||||
* **N.B.** As mentioned in the v0.12.0 release, old registration files may
|
|
||||||
have `[0-9]+` in the `users` regex. You must change it to `.+`, as the new
|
|
||||||
`lid` identifiers are bridged as `lid-<number>` instead of just `<phone number>`.
|
|
||||||
* Updated Docker image to Alpine 3.22.
|
|
||||||
* Fixed network errors on first connect not triggering automatic reconnect.
|
|
||||||
* Fixed animated sticker zips not being extracted when using direct media.
|
|
||||||
|
|
||||||
# v0.12.1 (2025-05-16)
|
|
||||||
|
|
||||||
* Added prefix to identify forwarded messages on WhatsApp.
|
|
||||||
* Updated mime type of unconverted animated stickers to `video/lottie+json`
|
|
||||||
which is now registered with IANA.
|
|
||||||
* Changed relogin command to not require entering phone number twice when using
|
|
||||||
phone code login.
|
|
||||||
* Fixed outgoing messages being rejected if they replied to a fake message
|
|
||||||
generated by the bridge.
|
|
||||||
* Fixed backfilling messages in existing portals after relogining.
|
|
||||||
|
|
||||||
# v0.12.0 (2025-04-16)
|
|
||||||
|
|
||||||
* Migrated Signal session store to use new `@lid` identifiers to support future
|
|
||||||
chats that don't expose phone numbers.
|
|
||||||
* **N.B.** Old registration files may have `[0-9]+` in the `users` regex. You
|
|
||||||
must change it to `.+`, as the new `lid` identifiers are bridged as
|
|
||||||
`lid-<number>` instead of just `<phone number>`.
|
|
||||||
* Added fallbacks for various business message types.
|
|
||||||
* Added support for bridging invites, kicks and leaves in groups.
|
|
||||||
* Re-added `invite-link`, `join` and `sync` commands for groups.
|
|
||||||
* Fixed bridging chats with Meta AI.
|
|
||||||
|
|
||||||
# v0.11.4 (2025-03-16)
|
# v0.11.4 (2025-03-16)
|
||||||
|
|
||||||
* Fixed edits being bridged multiple times if a single chat had multiple
|
* Fixed edits being bridged multiple times if a single chat had multiple
|
||||||
|
|
|
||||||
|
|
@ -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.21 AS builder
|
||||||
|
|
||||||
RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev
|
RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev
|
||||||
|
|
||||||
|
|
@ -6,12 +6,12 @@ COPY . /build
|
||||||
WORKDIR /build
|
WORKDIR /build
|
||||||
RUN ./build.sh
|
RUN ./build.sh
|
||||||
|
|
||||||
FROM alpine:3.24
|
FROM alpine:3.21
|
||||||
|
|
||||||
ENV UID=1337 \
|
ENV UID=1337 \
|
||||||
GID=1337
|
GID=1337
|
||||||
|
|
||||||
RUN apk add --no-cache ffmpeg su-exec ca-certificates olm bash jq curl yq-go lottieconverter
|
RUN apk add --no-cache ffmpeg su-exec ca-certificates olm bash jq yq curl
|
||||||
|
|
||||||
COPY --from=builder /build/mautrix-whatsapp /usr/bin/mautrix-whatsapp
|
COPY --from=builder /build/mautrix-whatsapp /usr/bin/mautrix-whatsapp
|
||||||
COPY --from=builder /build/docker-run.sh /docker-run.sh
|
COPY --from=builder /build/docker-run.sh /docker-run.sh
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
ARG DOCKER_HUB="docker.io"
|
FROM alpine:3.21
|
||||||
|
|
||||||
FROM ${DOCKER_HUB}/alpine:3.24
|
|
||||||
|
|
||||||
ENV UID=1337 \
|
ENV UID=1337 \
|
||||||
GID=1337
|
GID=1337
|
||||||
|
|
||||||
RUN apk add --no-cache ffmpeg su-exec ca-certificates bash jq curl yq-go lottieconverter
|
RUN apk add --no-cache ffmpeg su-exec ca-certificates bash jq curl yq-go
|
||||||
|
|
||||||
ARG EXECUTABLE=./mautrix-whatsapp
|
ARG EXECUTABLE=./mautrix-whatsapp
|
||||||
COPY $EXECUTABLE /usr/bin/mautrix-whatsapp
|
COPY $EXECUTABLE /usr/bin/mautrix-whatsapp
|
||||||
|
|
|
||||||
|
|
@ -14,10 +14,10 @@
|
||||||
* [x] Typing notifications
|
* [x] Typing notifications
|
||||||
* [x] Read receipts
|
* [x] Read receipts
|
||||||
* [ ] Power level
|
* [ ] Power level
|
||||||
* [x] Membership actions
|
* [ ] Membership actions
|
||||||
* [x] Invite
|
* [ ] Invite
|
||||||
* [x] Leave
|
* [ ] Leave
|
||||||
* [x] Kick
|
* [ ] Kick
|
||||||
* [ ] Room metadata changes
|
* [ ] Room metadata changes
|
||||||
* [ ] Name
|
* [ ] Name
|
||||||
* [ ] Avatar
|
* [ ] Avatar
|
||||||
|
|
|
||||||
4
build.sh
4
build.sh
|
|
@ -1,2 +1,4 @@
|
||||||
#!/bin/sh
|
#!/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
|
||||||
|
|
|
||||||
70
cmd/mautrix-whatsapp/legacymigrate.go
Normal file
70
cmd/mautrix-whatsapp/legacymigrate.go
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
up "go.mau.fi/util/configupgrade"
|
||||||
|
"go.mau.fi/util/dbutil/litestream"
|
||||||
|
"maunium.net/go/mautrix/bridgev2/bridgeconfig"
|
||||||
|
)
|
||||||
|
|
||||||
|
const legacyMigrateRenameTables = `
|
||||||
|
ALTER TABLE backfill_queue RENAME TO backfill_queue_old;
|
||||||
|
ALTER TABLE backfill_state RENAME TO backfill_state_old;
|
||||||
|
ALTER TABLE disappearing_message RENAME TO disappearing_message_old;
|
||||||
|
ALTER TABLE history_sync_message RENAME TO history_sync_message_old;
|
||||||
|
ALTER TABLE history_sync_conversation RENAME TO history_sync_conversation_old;
|
||||||
|
ALTER TABLE media_backfill_requests RENAME TO media_backfill_requests_old;
|
||||||
|
ALTER TABLE poll_option_id RENAME TO poll_option_id_old;
|
||||||
|
ALTER TABLE user_portal RENAME TO user_portal_old;
|
||||||
|
ALTER TABLE portal RENAME TO portal_old;
|
||||||
|
ALTER TABLE puppet RENAME TO puppet_old;
|
||||||
|
ALTER TABLE message RENAME TO message_old;
|
||||||
|
ALTER TABLE reaction RENAME TO reaction_old;
|
||||||
|
ALTER TABLE "user" RENAME TO user_old;
|
||||||
|
`
|
||||||
|
|
||||||
|
//go:embed legacymigrate.sql
|
||||||
|
var legacyMigrateCopyData string
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
litestream.Functions["split_part"] = func(input, delimiter string, partNum int) string {
|
||||||
|
// split_part is 1-indexed
|
||||||
|
partNum--
|
||||||
|
parts := strings.Split(input, delimiter)
|
||||||
|
if len(parts) <= partNum {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return parts[partNum]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateLegacyConfig(helper up.Helper) {
|
||||||
|
helper.Set(up.Str, "maunium.net/go/mautrix-whatsapp", "encryption", "pickle_key")
|
||||||
|
bridgeconfig.CopyToOtherLocation(helper, up.Str, []string{"whatsapp", "os_name"}, []string{"network", "os_name"})
|
||||||
|
bridgeconfig.CopyToOtherLocation(helper, up.Str, []string{"whatsapp", "browser_name"}, []string{"network", "browser_name"})
|
||||||
|
bridgeconfig.CopyToOtherLocation(helper, up.Str, []string{"whatsapp", "proxy"}, []string{"network", "proxy"})
|
||||||
|
bridgeconfig.CopyToOtherLocation(helper, up.Str, []string{"whatsapp", "get_proxy_url"}, []string{"network", "get_proxy_url"})
|
||||||
|
bridgeconfig.CopyToOtherLocation(helper, up.Bool, []string{"whatsapp", "proxy_only_login"}, []string{"network", "proxy_only_login"})
|
||||||
|
bridgeconfig.CopyToOtherLocation(helper, up.Str, []string{"bridge", "displayname_template"}, []string{"network", "displayname_template"})
|
||||||
|
bridgeconfig.CopyToOtherLocation(helper, up.Bool, []string{"bridge", "call_start_notices"}, []string{"network", "call_start_notices"})
|
||||||
|
bridgeconfig.CopyToOtherLocation(helper, up.Bool, []string{"bridge", "send_presence_on_typing"}, []string{"network", "send_presence_on_typing"})
|
||||||
|
bridgeconfig.CopyToOtherLocation(helper, up.Bool, []string{"bridge", "enable_status_broadcast"}, []string{"network", "enable_status_broadcast"})
|
||||||
|
bridgeconfig.CopyToOtherLocation(helper, up.Bool, []string{"bridge", "disable_status_broadcast_send"}, []string{"network", "disable_status_broadcast_send"})
|
||||||
|
bridgeconfig.CopyToOtherLocation(helper, up.Bool, []string{"bridge", "mute_status_broadcast"}, []string{"network", "mute_status_broadcast"})
|
||||||
|
bridgeconfig.CopyToOtherLocation(helper, up.Str, []string{"bridge", "status_broadcast_tag"}, []string{"network", "status_broadcast_tag"})
|
||||||
|
bridgeconfig.CopyToOtherLocation(helper, up.Bool, []string{"bridge", "whatsapp_thumbnail"}, []string{"network", "whatsapp_thumbnail"})
|
||||||
|
bridgeconfig.CopyToOtherLocation(helper, up.Bool, []string{"bridge", "url_previews"}, []string{"network", "url_previews"})
|
||||||
|
bridgeconfig.CopyToOtherLocation(helper, up.Bool, []string{"bridge", "extev_polls"}, []string{"network", "extev_polls"})
|
||||||
|
bridgeconfig.CopyToOtherLocation(helper, up.Bool, []string{"bridge", "force_active_delivery_receipts"}, []string{"network", "force_active_delivery_receipts"})
|
||||||
|
bridgeconfig.CopyToOtherLocation(helper, up.Int, []string{"bridge", "history_sync", "max_initial_conversations"}, []string{"network", "history_sync", "max_initial_conversations"})
|
||||||
|
bridgeconfig.CopyToOtherLocation(helper, up.Bool, []string{"bridge", "history_sync", "request_full_sync"}, []string{"network", "history_sync", "request_full_sync"})
|
||||||
|
bridgeconfig.CopyToOtherLocation(helper, up.Int, []string{"bridge", "history_sync", "full_sync_config", "days_limit"}, []string{"network", "history_sync", "full_sync_config", "days_limit"})
|
||||||
|
bridgeconfig.CopyToOtherLocation(helper, up.Int, []string{"bridge", "history_sync", "full_sync_config", "size_limit_mb"}, []string{"network", "history_sync", "full_sync_config", "size_limit_mb"})
|
||||||
|
bridgeconfig.CopyToOtherLocation(helper, up.Int, []string{"bridge", "history_sync", "full_sync_config", "storage_quota_mb"}, []string{"network", "history_sync", "full_sync_config", "storage_quota_mb"})
|
||||||
|
bridgeconfig.CopyToOtherLocation(helper, up.Bool, []string{"bridge", "history_sync", "media_requests", "auto_request_media"}, []string{"network", "history_sync", "media_requests", "auto_request_media"})
|
||||||
|
bridgeconfig.CopyToOtherLocation(helper, up.Str, []string{"bridge", "history_sync", "media_requests", "request_method"}, []string{"network", "history_sync", "media_requests", "request_method"})
|
||||||
|
bridgeconfig.CopyToOtherLocation(helper, up.Int, []string{"bridge", "history_sync", "media_requests", "request_local_time"}, []string{"network", "history_sync", "media_requests", "request_local_time"})
|
||||||
|
bridgeconfig.CopyToOtherLocation(helper, up.Int, []string{"bridge", "history_sync", "media_requests", "max_async_handle"}, []string{"network", "history_sync", "media_requests", "max_async_handle"})
|
||||||
|
}
|
||||||
214
cmd/mautrix-whatsapp/legacymigrate.sql
Normal file
214
cmd/mautrix-whatsapp/legacymigrate.sql
Normal file
|
|
@ -0,0 +1,214 @@
|
||||||
|
INSERT INTO "user" (bridge_id, mxid, management_room, access_token)
|
||||||
|
SELECT '', mxid, management_room, ''
|
||||||
|
FROM user_old;
|
||||||
|
|
||||||
|
UPDATE "user" SET access_token=COALESCE((SELECT access_token FROM puppet_old WHERE custom_mxid="user".mxid AND access_token<>'' LIMIT 1), '');
|
||||||
|
|
||||||
|
INSERT INTO user_login (bridge_id, user_mxid, id, remote_name, space_room, metadata, remote_profile)
|
||||||
|
SELECT
|
||||||
|
'', -- bridge_id
|
||||||
|
mxid, -- user_mxid
|
||||||
|
username, -- id
|
||||||
|
'+' || username, -- remote_name
|
||||||
|
space_room,
|
||||||
|
-- only: postgres
|
||||||
|
jsonb_build_object
|
||||||
|
-- only: sqlite (line commented)
|
||||||
|
-- json_object
|
||||||
|
(
|
||||||
|
'wa_device_id', device,
|
||||||
|
'phone_last_seen', phone_last_seen,
|
||||||
|
'phone_last_pinged', phone_last_pinged,
|
||||||
|
'timezone', timezone
|
||||||
|
), -- metadata
|
||||||
|
'{}' -- remote_profile
|
||||||
|
FROM user_old
|
||||||
|
WHERE username<>'' AND device<>0;
|
||||||
|
|
||||||
|
INSERT INTO ghost (
|
||||||
|
bridge_id, id, name, avatar_id, avatar_hash, avatar_mxc,
|
||||||
|
name_set, avatar_set, contact_info_set, is_bot, identifiers, metadata
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'', -- bridge_id
|
||||||
|
username, -- id
|
||||||
|
COALESCE(displayname, ''), -- name
|
||||||
|
COALESCE(avatar, ''), -- avatar_id
|
||||||
|
'', -- avatar_hash
|
||||||
|
COALESCE(avatar_url, ''), -- avatar_mxc
|
||||||
|
name_set,
|
||||||
|
avatar_set,
|
||||||
|
contact_info_set,
|
||||||
|
false, -- is_bot
|
||||||
|
'[]', -- identifiers
|
||||||
|
-- only: postgres
|
||||||
|
jsonb_build_object
|
||||||
|
-- only: sqlite (line commented)
|
||||||
|
-- json_object
|
||||||
|
(
|
||||||
|
'last_sync', last_sync
|
||||||
|
-- TODO name quality
|
||||||
|
) -- metadata
|
||||||
|
FROM puppet_old;
|
||||||
|
|
||||||
|
-- Some messages don't have senders, so insert an empty ghost to match the foreign key constraint.
|
||||||
|
INSERT INTO ghost (bridge_id, id, name, avatar_id, avatar_hash, avatar_mxc, name_set, avatar_set, contact_info_set, is_bot, identifiers, metadata)
|
||||||
|
VALUES ('', '', '', '', '', '', false, false, false, false, '[]', '{}')
|
||||||
|
ON CONFLICT (bridge_id, id) DO NOTHING;
|
||||||
|
|
||||||
|
DELETE FROM portal_old WHERE jid LIKE '%@s.whatsapp.net' AND (receiver='' OR receiver IS NULL) and mxid IS NULL;
|
||||||
|
|
||||||
|
INSERT INTO portal (
|
||||||
|
bridge_id, id, receiver, mxid, parent_id, parent_receiver, relay_bridge_id, relay_login_id, other_user_id,
|
||||||
|
name, topic, avatar_id, avatar_hash, avatar_mxc, name_set, avatar_set, topic_set,
|
||||||
|
name_is_custom, in_space, room_type, disappear_type, disappear_timer, metadata
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'', -- bridge_id
|
||||||
|
jid, -- id
|
||||||
|
CASE WHEN receiver LIKE '%@s.whatsapp.net' THEN replace(receiver, '@s.whatsapp.net', '') ELSE '' END, -- receiver
|
||||||
|
mxid,
|
||||||
|
CASE WHEN EXISTS(SELECT 1 FROM portal_old WHERE jid=parent_group) THEN parent_group ELSE NULL END, -- parent_id
|
||||||
|
'', -- parent_receiver
|
||||||
|
CASE WHEN relay_user_id<>'' THEN '' END, -- relay_bridge_id
|
||||||
|
(SELECT id FROM user_login WHERE user_mxid=relay_user_id), -- relay_login_id
|
||||||
|
CASE WHEN jid LIKE '%@s.whatsapp.net' THEN replace(jid, '@s.whatsapp.net', '') ELSE '' END, -- other_user_id
|
||||||
|
name,
|
||||||
|
topic,
|
||||||
|
avatar, -- avatar_id
|
||||||
|
'', -- avatar_hash
|
||||||
|
COALESCE(avatar_url, ''), -- avatar_mxc
|
||||||
|
name_set,
|
||||||
|
avatar_set,
|
||||||
|
topic_set,
|
||||||
|
jid NOT LIKE '%@s.whatsapp.net', -- name_is_custom
|
||||||
|
in_space,
|
||||||
|
CASE
|
||||||
|
WHEN is_parent THEN 'space'
|
||||||
|
WHEN jid LIKE '%@s.whatsapp.net' THEN 'dm'
|
||||||
|
ELSE ''
|
||||||
|
END, -- room_type
|
||||||
|
CASE WHEN expiration_time>0 THEN 'after_read' END, -- disappear_type
|
||||||
|
CASE WHEN expiration_time > 0 THEN expiration_time * 1000000000 END, -- disappear_timer
|
||||||
|
-- only: postgres
|
||||||
|
jsonb_build_object
|
||||||
|
-- only: sqlite (line commented)
|
||||||
|
-- json_object
|
||||||
|
(
|
||||||
|
'last_sync', last_sync
|
||||||
|
) -- metadata
|
||||||
|
FROM portal_old;
|
||||||
|
|
||||||
|
-- only: sqlite
|
||||||
|
DELETE FROM user_portal_old WHERE rowid IN (SELECT rowid FROM pragma_foreign_key_check('user_portal_old'));
|
||||||
|
|
||||||
|
INSERT INTO user_portal (bridge_id, user_mxid, login_id, portal_id, portal_receiver, in_space, preferred, last_read)
|
||||||
|
SELECT
|
||||||
|
'', -- bridge_id
|
||||||
|
user_mxid,
|
||||||
|
(SELECT id FROM user_login WHERE user_login.user_mxid=user_portal_old.user_mxid), -- login_id
|
||||||
|
portal_jid, -- portal_id
|
||||||
|
CASE WHEN portal_receiver LIKE '%@s.whatsapp.net' THEN replace(portal_receiver, '@s.whatsapp.net', '') ELSE '' END, -- portal_receiver
|
||||||
|
in_space,
|
||||||
|
false, -- preferred
|
||||||
|
last_read_ts * 1000000000 -- last_read
|
||||||
|
FROM user_portal_old WHERE EXISTS(SELECT 1 FROM user_login WHERE user_login.user_mxid=user_portal_old.user_mxid);
|
||||||
|
|
||||||
|
ALTER TABLE message_old ADD COLUMN combined_id TEXT;
|
||||||
|
DELETE FROM message_old WHERE sender IS NULL;
|
||||||
|
UPDATE message_old SET combined_id = chat_jid || ':' || (
|
||||||
|
CASE WHEN sender LIKE '%:%@s.whatsapp.net'
|
||||||
|
THEN (split_part(replace(sender, '@s.whatsapp.net', ''), ':', 1) || '@s.whatsapp.net')
|
||||||
|
ELSE sender
|
||||||
|
END
|
||||||
|
) || ':' || jid;
|
||||||
|
DELETE FROM message_old WHERE timestamp<0;
|
||||||
|
-- only: sqlite for next 2 lines
|
||||||
|
DELETE FROM message_old WHERE rowid IN (SELECT rowid FROM pragma_foreign_key_check('message_old'));
|
||||||
|
DELETE FROM reaction_old WHERE rowid IN (SELECT rowid FROM pragma_foreign_key_check('reaction_old'));
|
||||||
|
DELETE FROM message_old WHERE sender NOT LIKE '%@s.whatsapp.net' AND sender<>chat_jid;
|
||||||
|
DELETE FROM reaction_old WHERE sender NOT LIKE '%@s.whatsapp.net';
|
||||||
|
DELETE FROM reaction_old WHERE NOT EXISTS(SELECT 1 FROM puppet_old WHERE username=replace(sender, '@s.whatsapp.net', ''));
|
||||||
|
|
||||||
|
INSERT INTO message (
|
||||||
|
bridge_id, id, part_id, mxid, room_id, room_receiver, sender_id, sender_mxid, timestamp, edit_count, metadata
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'', -- bridge_id
|
||||||
|
combined_id, -- id
|
||||||
|
'', -- part_id
|
||||||
|
mxid,
|
||||||
|
chat_jid, -- room_id
|
||||||
|
CASE WHEN chat_receiver LIKE '%@s.whatsapp.net' THEN replace(chat_receiver, '@s.whatsapp.net', '') ELSE '' END, -- room_receiver
|
||||||
|
CASE WHEN sender=chat_jid AND sender NOT LIKE '%@s.whatsapp.net'
|
||||||
|
THEN ''
|
||||||
|
ELSE split_part(split_part(replace(sender, '@s.whatsapp.net', ''), ':', 1), '.', 1)
|
||||||
|
END, -- sender_id
|
||||||
|
sender_mxid, -- sender_mxid
|
||||||
|
timestamp * 1000000000, -- timestamp
|
||||||
|
0, -- edit_count
|
||||||
|
-- only: postgres
|
||||||
|
jsonb_build_object
|
||||||
|
-- only: sqlite (line commented)
|
||||||
|
-- json_object
|
||||||
|
(
|
||||||
|
'sender_device_id', CAST(nullif(split_part(replace(sender, '@s.whatsapp.net', ''), ':', 2), '') AS INTEGER),
|
||||||
|
'broadcast_list_jid', broadcast_list_jid,
|
||||||
|
'error', CAST(error AS TEXT)
|
||||||
|
) -- metadata
|
||||||
|
FROM message_old;
|
||||||
|
|
||||||
|
INSERT INTO reaction (
|
||||||
|
bridge_id, message_id, message_part_id, sender_id, emoji_id, room_id, room_receiver, mxid, timestamp, emoji, metadata
|
||||||
|
)
|
||||||
|
SELECT
|
||||||
|
'', -- bridge_id
|
||||||
|
message_old.combined_id, -- message_id
|
||||||
|
'', -- message_part_id
|
||||||
|
replace(reaction_old.sender, '@s.whatsapp.net', ''), -- sender_id
|
||||||
|
'', -- emoji_id
|
||||||
|
reaction_old.chat_jid, -- room_id
|
||||||
|
CASE WHEN reaction_old.chat_receiver LIKE '%@s.whatsapp.net' THEN replace(reaction_old.chat_receiver, '@s.whatsapp.net', '') ELSE '' END, -- room_receiver
|
||||||
|
reaction_old.mxid,
|
||||||
|
0, -- timestamp
|
||||||
|
'', -- emoji
|
||||||
|
-- only: postgres
|
||||||
|
jsonb_build_object
|
||||||
|
-- only: sqlite (line commented)
|
||||||
|
-- json_object
|
||||||
|
(
|
||||||
|
'sender_device_id', CAST(nullif(split_part(replace(reaction_old.sender, '@s.whatsapp.net', ''), ':', 2), '') AS INTEGER)
|
||||||
|
) -- metadata
|
||||||
|
FROM reaction_old
|
||||||
|
LEFT JOIN message_old
|
||||||
|
ON reaction_old.chat_jid = message_old.chat_jid
|
||||||
|
AND reaction_old.chat_receiver = message_old.chat_receiver
|
||||||
|
AND reaction_old.target_jid = message_old.jid;
|
||||||
|
|
||||||
|
INSERT INTO disappearing_message (bridge_id, mx_room, mxid, type, timer, disappear_at)
|
||||||
|
SELECT
|
||||||
|
'', -- bridge_id
|
||||||
|
room_id,
|
||||||
|
event_id,
|
||||||
|
'after_read',
|
||||||
|
expire_in * 1000000, -- timer
|
||||||
|
expire_at * 1000000 -- disappear_at
|
||||||
|
FROM disappearing_message_old;
|
||||||
|
|
||||||
|
INSERT INTO whatsapp_poll_option_id (bridge_id, msg_mxid, opt_id, opt_hash)
|
||||||
|
SELECT '', msg_mxid, opt_id, opt_hash
|
||||||
|
FROM poll_option_id_old;
|
||||||
|
|
||||||
|
DROP TABLE backfill_queue_old;
|
||||||
|
DROP TABLE backfill_state_old;
|
||||||
|
DROP TABLE disappearing_message_old;
|
||||||
|
DROP TABLE history_sync_message_old;
|
||||||
|
DROP TABLE history_sync_conversation_old;
|
||||||
|
DROP TABLE media_backfill_requests_old;
|
||||||
|
DROP TABLE poll_option_id_old;
|
||||||
|
DROP TABLE user_portal_old;
|
||||||
|
DROP TABLE reaction_old;
|
||||||
|
DROP TABLE message_old;
|
||||||
|
DROP TABLE puppet_old;
|
||||||
|
DROP TABLE portal_old;
|
||||||
|
DROP TABLE user_old;
|
||||||
308
cmd/mautrix-whatsapp/legacyprovision.go
Normal file
308
cmd/mautrix-whatsapp/legacyprovision.go
Normal file
|
|
@ -0,0 +1,308 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net/http"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/gorilla/websocket"
|
||||||
|
"github.com/rs/zerolog/hlog"
|
||||||
|
"go.mau.fi/util/exhttp"
|
||||||
|
"go.mau.fi/whatsmeow/types"
|
||||||
|
"maunium.net/go/mautrix"
|
||||||
|
"maunium.net/go/mautrix/bridgev2"
|
||||||
|
"maunium.net/go/mautrix/bridgev2/matrix"
|
||||||
|
"maunium.net/go/mautrix/bridgev2/status"
|
||||||
|
"maunium.net/go/mautrix/id"
|
||||||
|
|
||||||
|
"go.mau.fi/mautrix-whatsapp/pkg/connector"
|
||||||
|
"go.mau.fi/mautrix-whatsapp/pkg/waid"
|
||||||
|
)
|
||||||
|
|
||||||
|
var upgrader = websocket.Upgrader{
|
||||||
|
CheckOrigin: func(r *http.Request) bool {
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
Subprotocols: []string{"net.maunium.whatsapp.login"},
|
||||||
|
}
|
||||||
|
|
||||||
|
func legacyProvAuth(r *http.Request) string {
|
||||||
|
if !strings.HasSuffix(r.URL.Path, "/v1/login") {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
authParts := strings.Split(r.Header.Get("Sec-WebSocket-Protocol"), ",")
|
||||||
|
for _, part := range authParts {
|
||||||
|
part = strings.TrimSpace(part)
|
||||||
|
if strings.HasPrefix(part, "net.maunium.whatsapp.auth-") {
|
||||||
|
return strings.TrimPrefix(part, "net.maunium.whatsapp.auth-")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func respondWebsocketWithError(conn *websocket.Conn, err error, message string) {
|
||||||
|
var mautrixRespErr mautrix.RespError
|
||||||
|
var bv2RespErr bridgev2.RespError
|
||||||
|
if errors.As(err, &bv2RespErr) {
|
||||||
|
mautrixRespErr = mautrix.RespError(bv2RespErr)
|
||||||
|
} else if !errors.As(err, &mautrixRespErr) {
|
||||||
|
mautrixRespErr = mautrix.RespError{
|
||||||
|
Err: message,
|
||||||
|
ErrCode: "M_UNKNOWN",
|
||||||
|
StatusCode: http.StatusInternalServerError,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ = conn.WriteJSON(&mautrixRespErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
var notNumbers = regexp.MustCompile("[^0-9]")
|
||||||
|
|
||||||
|
func legacyProvLogin(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log := hlog.FromRequest(r)
|
||||||
|
|
||||||
|
conn, err := upgrader.Upgrade(w, r, nil)
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msg("Failed to upgrade connection to websocket")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
err := conn.Close()
|
||||||
|
if err != nil {
|
||||||
|
log.Debug().Err(err).Msg("Error closing websocket")
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// Read everything so SetCloseHandler() works
|
||||||
|
for {
|
||||||
|
_, _, err = conn.ReadMessage()
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
conn.SetCloseHandler(func(code int, text string) error {
|
||||||
|
log.Debug().Int("close_code", code).Msg("Login websocket closed, cancelling login")
|
||||||
|
cancel()
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
user := m.Matrix.Provisioning.GetUser(r)
|
||||||
|
loginFlowID := connector.LoginFlowIDQR
|
||||||
|
phoneNum := r.URL.Query().Get("phone_number")
|
||||||
|
if phoneNum != "" {
|
||||||
|
phoneNum = notNumbers.ReplaceAllString(phoneNum, "")
|
||||||
|
if len(phoneNum) < 7 || strings.HasPrefix(phoneNum, "0") {
|
||||||
|
errorMsg := "Invalid phone number"
|
||||||
|
if len(phoneNum) > 6 {
|
||||||
|
errorMsg = "Please enter the phone number in international format"
|
||||||
|
}
|
||||||
|
_ = conn.WriteJSON(Error{
|
||||||
|
Error: errorMsg,
|
||||||
|
ErrCode: "invalid phone number",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
loginFlowID = connector.LoginFlowIDPhone
|
||||||
|
}
|
||||||
|
login, err := c.CreateLogin(ctx, user, loginFlowID)
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msg("Failed to create login")
|
||||||
|
respondWebsocketWithError(conn, err, "Failed to create login")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
waLogin := login.(*connector.WALogin)
|
||||||
|
waLogin.Timezone = r.URL.Query().Get("tz")
|
||||||
|
step, err := waLogin.Start(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msg("Failed to start login")
|
||||||
|
respondWebsocketWithError(conn, err, "Failed to start login")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if phoneNum != "" {
|
||||||
|
if step.StepID != connector.LoginStepIDPhoneNumber {
|
||||||
|
respondWebsocketWithError(conn, errors.New("unexpected step"), "Unexpected step while starting phone number login")
|
||||||
|
waLogin.Cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
step, err = waLogin.SubmitUserInput(ctx, map[string]string{"phone_number": phoneNum})
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msg("Failed to submit phone number")
|
||||||
|
respondWebsocketWithError(conn, err, "Failed to start phone code login")
|
||||||
|
return
|
||||||
|
} else if step.StepID != connector.LoginStepIDCode {
|
||||||
|
respondWebsocketWithError(conn, errors.New("unexpected step"), "Unexpected step after submitting phone number")
|
||||||
|
waLogin.Cancel()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = conn.WriteJSON(map[string]any{
|
||||||
|
"pairing_code": step.DisplayAndWaitParams.Data,
|
||||||
|
"timeout": 180,
|
||||||
|
})
|
||||||
|
} else if step.StepID != connector.LoginStepIDQR {
|
||||||
|
respondWebsocketWithError(conn, errors.New("unexpected step"), "Unexpected step while starting QR login")
|
||||||
|
waLogin.Cancel()
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
_ = conn.WriteJSON(map[string]any{
|
||||||
|
"code": step.DisplayAndWaitParams.Data,
|
||||||
|
"timeout": 60,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
step, err = waLogin.Wait(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Err(err).Msg("Failed to wait for login")
|
||||||
|
respondWebsocketWithError(conn, err, "Failed to wait for login")
|
||||||
|
} else if step.StepID == connector.LoginStepIDQR {
|
||||||
|
_ = conn.WriteJSON(map[string]any{
|
||||||
|
"code": step.DisplayAndWaitParams.Data,
|
||||||
|
"timeout": 20,
|
||||||
|
})
|
||||||
|
continue
|
||||||
|
} else if step.StepID != connector.LoginStepIDComplete {
|
||||||
|
respondWebsocketWithError(conn, errors.New("unexpected step"), "Unexpected step while waiting for login")
|
||||||
|
waLogin.Cancel()
|
||||||
|
} else {
|
||||||
|
// TODO delete old logins
|
||||||
|
_ = conn.WriteJSON(map[string]any{
|
||||||
|
"success": true,
|
||||||
|
"jid": waid.ParseUserLoginID(step.CompleteParams.UserLoginID, step.CompleteParams.UserLogin.Metadata.(*waid.UserLoginMetadata).WADeviceID).String(),
|
||||||
|
"platform": step.CompleteParams.UserLogin.Client.(*connector.WhatsAppClient).Device.Platform,
|
||||||
|
"phone": step.CompleteParams.UserLogin.RemoteProfile.Phone,
|
||||||
|
})
|
||||||
|
go handleLoginComplete(context.WithoutCancel(ctx), user, step.CompleteParams.UserLogin)
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
func handleLoginComplete(ctx context.Context, user *bridgev2.User, newLogin *bridgev2.UserLogin) {
|
||||||
|
allLogins := user.GetUserLogins()
|
||||||
|
for _, login := range allLogins {
|
||||||
|
if login.ID != newLogin.ID {
|
||||||
|
login.Delete(ctx, status.BridgeState{StateEvent: status.StateLoggedOut, Reason: "LOGIN_OVERRIDDEN"}, bridgev2.DeleteOpts{})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func legacyProvLogout(w http.ResponseWriter, r *http.Request) {
|
||||||
|
user := m.Matrix.Provisioning.GetUser(r)
|
||||||
|
allLogins := user.GetUserLogins()
|
||||||
|
if len(allLogins) == 0 {
|
||||||
|
exhttp.WriteJSONResponse(w, http.StatusOK, Error{
|
||||||
|
Error: "You're not logged in",
|
||||||
|
ErrCode: "not logged in",
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
for _, login := range allLogins {
|
||||||
|
// Intentionally don't delete the user login, only logout remote
|
||||||
|
login.Client.(*connector.WhatsAppClient).LogoutRemote(r.Context())
|
||||||
|
}
|
||||||
|
exhttp.WriteJSONResponse(w, http.StatusOK, Response{true, "Logged out successfully"})
|
||||||
|
}
|
||||||
|
|
||||||
|
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(); 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 := mux.Vars(r)["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,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
@ -1,9 +1,13 @@
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"maunium.net/go/mautrix/bridgev2/bridgeconfig"
|
||||||
"maunium.net/go/mautrix/bridgev2/matrix/mxmain"
|
"maunium.net/go/mautrix/bridgev2/matrix/mxmain"
|
||||||
|
|
||||||
"go.mau.fi/mautrix-whatsapp/pkg/connector"
|
"go.mau.fi/mautrix-whatsapp/pkg/connector"
|
||||||
|
"go.mau.fi/mautrix-whatsapp/pkg/connector/wadb/upgrades"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Information to find out exactly which commit the bridge was built from.
|
// Information to find out exactly which commit the bridge was built from.
|
||||||
|
|
@ -14,16 +18,39 @@ var (
|
||||||
BuildTime = "unknown"
|
BuildTime = "unknown"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var c = &connector.WhatsAppConnector{}
|
||||||
var m = mxmain.BridgeMain{
|
var m = mxmain.BridgeMain{
|
||||||
Name: "mautrix-whatsapp",
|
Name: "mautrix-whatsapp",
|
||||||
URL: "https://github.com/mautrix/whatsapp",
|
URL: "https://github.com/mautrix/whatsapp",
|
||||||
Description: "A Matrix-WhatsApp puppeting bridge.",
|
Description: "A Matrix-WhatsApp puppeting bridge.",
|
||||||
Version: "26.10",
|
Version: "0.11.4",
|
||||||
SemCalVer: true,
|
Connector: c,
|
||||||
Connector: &connector.WhatsAppConnector{},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
bridgeconfig.HackyMigrateLegacyNetworkConfig = migrateLegacyConfig
|
||||||
|
m.PostInit = func() {
|
||||||
|
m.CheckLegacyDB(
|
||||||
|
57,
|
||||||
|
"v0.8.6",
|
||||||
|
"v0.11.0",
|
||||||
|
m.LegacyMigrateWithAnotherUpgrader(
|
||||||
|
legacyMigrateRenameTables, legacyMigrateCopyData, 17,
|
||||||
|
upgrades.Table, "whatsapp_version", 3,
|
||||||
|
),
|
||||||
|
true,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
m.PostStart = func() {
|
||||||
|
if m.Matrix.Provisioning != nil {
|
||||||
|
m.Matrix.Provisioning.Router.HandleFunc("/v1/login", legacyProvLogin).Methods(http.MethodGet)
|
||||||
|
m.Matrix.Provisioning.Router.HandleFunc("/v1/logout", legacyProvLogout).Methods(http.MethodPost)
|
||||||
|
m.Matrix.Provisioning.Router.HandleFunc("/v1/contacts", legacyProvContacts).Methods(http.MethodGet)
|
||||||
|
m.Matrix.Provisioning.Router.HandleFunc("/v1/resolve_identifier/{number}", legacyProvResolveIdentifier).Methods(http.MethodGet)
|
||||||
|
m.Matrix.Provisioning.Router.HandleFunc("/v1/pm/{number}", legacyProvResolveIdentifier).Methods(http.MethodPost)
|
||||||
|
m.Matrix.Provisioning.GetAuthFromRequest = legacyProvAuth
|
||||||
|
}
|
||||||
|
}
|
||||||
m.InitVersion(Tag, Commit, BuildTime)
|
m.InitVersion(Tag, Commit, BuildTime)
|
||||||
m.Run()
|
m.Run()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
61
go.mod
61
go.mod
|
|
@ -1,53 +1,48 @@
|
||||||
module go.mau.fi/mautrix-whatsapp
|
module go.mau.fi/mautrix-whatsapp
|
||||||
|
|
||||||
go 1.26.0
|
go 1.23.0
|
||||||
|
|
||||||
toolchain go1.27.2
|
toolchain go1.24.1
|
||||||
|
|
||||||
tool go.mau.fi/util/cmd/maubuild
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/google/uuid v1.6.0
|
github.com/gorilla/mux v1.8.0
|
||||||
github.com/lib/pq v1.12.3
|
github.com/gorilla/websocket v1.5.0
|
||||||
github.com/rs/zerolog v1.35.1
|
github.com/lib/pq v1.10.9
|
||||||
github.com/tidwall/gjson v1.20.0
|
github.com/rs/zerolog v1.34.0
|
||||||
github.com/yuin/goldmark v1.8.6
|
go.mau.fi/util v0.8.6
|
||||||
go.mau.fi/util v0.11.0
|
go.mau.fi/webp v0.2.0
|
||||||
go.mau.fi/webp v0.3.0
|
go.mau.fi/whatsmeow v0.0.0-20250411192951-5ab78fadbf91
|
||||||
go.mau.fi/whatsmeow v0.0.0-20261026125351-1fbae988179a
|
golang.org/x/image v0.26.0
|
||||||
golang.org/x/image v0.47.0
|
golang.org/x/net v0.39.0
|
||||||
golang.org/x/net v0.61.0
|
golang.org/x/sync v0.13.0
|
||||||
golang.org/x/sync v0.24.0
|
google.golang.org/protobuf v1.36.6
|
||||||
google.golang.org/protobuf v1.36.12
|
|
||||||
gopkg.in/yaml.v3 v3.0.1
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
maunium.net/go/mautrix v0.32.1-0.20261105141340-842796acf445
|
maunium.net/go/mautrix v0.23.3-0.20250405234116-e675a3c09c38
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
filippo.io/edwards25519 v1.2.0 // indirect
|
filippo.io/edwards25519 v1.1.0 // indirect
|
||||||
github.com/beeper/argo-go v1.1.2 // indirect
|
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||||
github.com/coder/websocket v1.8.15 // indirect
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/coreos/go-systemd/v22 v22.7.0 // indirect
|
|
||||||
github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect
|
|
||||||
github.com/kr/pretty v0.3.1 // indirect
|
github.com/kr/pretty v0.3.1 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.52 // indirect
|
github.com/mattn/go-sqlite3 v1.14.24 // indirect
|
||||||
github.com/petermattis/goid v0.0.0-20260918085751-abfca077860b // indirect
|
github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203 // indirect
|
||||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||||
github.com/rs/xid v1.6.0 // indirect
|
github.com/rs/xid v1.6.0 // indirect
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
||||||
github.com/tidwall/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/pretty v1.2.1 // indirect
|
||||||
github.com/tidwall/sjson v1.2.5 // indirect
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
github.com/vektah/gqlparser/v2 v2.5.27 // indirect
|
github.com/yuin/goldmark v1.7.8 // indirect
|
||||||
go.mau.fi/libsignal v0.2.2 // indirect
|
go.mau.fi/libsignal v0.1.2 // indirect
|
||||||
go.mau.fi/zeroconfig v0.2.0 // indirect
|
go.mau.fi/zeroconfig v0.1.3 // indirect
|
||||||
golang.org/x/crypto v0.58.0 // indirect
|
golang.org/x/crypto v0.37.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e // indirect
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||||
golang.org/x/mod v0.42.0 // indirect
|
golang.org/x/sys v0.32.0 // indirect
|
||||||
golang.org/x/sys v0.49.0 // indirect
|
golang.org/x/text v0.24.0 // indirect
|
||||||
golang.org/x/text v0.43.0 // indirect
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||||
maunium.net/go/mauflag v1.0.0 // indirect
|
maunium.net/go/mauflag v1.0.0 // indirect
|
||||||
|
|
|
||||||
130
go.sum
130
go.sum
|
|
@ -1,24 +1,21 @@
|
||||||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||||
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
|
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||||
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
|
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
|
||||||
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/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
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 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI=
|
||||||
|
github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So=
|
||||||
|
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.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||||
|
|
@ -26,74 +23,75 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
|
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||||
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
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 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||||
|
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||||
|
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.52 h1:wVbm2Qnf4OXkqhBTSPuCRZDRnxfbVrrmiCEroVdog8U=
|
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||||
github.com/mattn/go-sqlite3 v1.14.52/go.mod h1:6JTjA44L93a0QCyJef5YvlPoKXntQPjzWv5gtm9sB6w=
|
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
github.com/petermattis/goid v0.0.0-20260918085751-abfca077860b h1:OzNsuVdSWGwXvWKTtChx9ve89k4cFJ6NxYM4Aw4/f+E=
|
github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203 h1:E7Kmf11E4K7B5hDti2K2NqPb1nlYlGYsu02S1JNd/Bs=
|
||||||
github.com/petermattis/goid v0.0.0-20260918085751-abfca077860b/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
|
github.com/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||||
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
|
||||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||||
github.com/stretchr/testify v1.12.0 h1:K6Mr6jO9JICuend/5xzTM03ydSV3vdNRYAdPSukj8uI=
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
github.com/stretchr/testify v1.12.0/go.mod h1:bOYBZb5qJ00vPzWfIqBUZPaxK8jWiXc6d3ErP4Ca9Gw=
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
github.com/tidwall/gjson v1.20.0 h1:+agJ3rEzKcCXKDKo0ml26UROcpcAlnZyjq+TjbY5Fto=
|
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||||
github.com/tidwall/gjson v1.20.0/go.mod h1:V37/opeE/JbLUOfH0QTXiNez2l0RUjYUhpT4szFQAfc=
|
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
|
|
||||||
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
|
||||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
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 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||||
github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=
|
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||||
github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
|
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||||
github.com/yuin/goldmark v1.8.6 h1:d0VcaP1sx9GkFVkoW+KtggpGi2KZ965i14b0+bDQST4=
|
go.mau.fi/libsignal v0.1.2 h1:Vs16DXWxSKyzVtI+EEXLCSy5pVWzzCzp/2eqFGvLyP0=
|
||||||
github.com/yuin/goldmark v1.8.6/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
go.mau.fi/libsignal v0.1.2/go.mod h1:JpnLSSJptn/s1sv7I56uEMywvz8x4YzxeF5OzdPb6PE=
|
||||||
go.mau.fi/libsignal v0.2.2 h1:QV+XdzQkm3x3aSG7FcqfGSZuFXz83pRZPBFaPygHbOU=
|
go.mau.fi/util v0.8.6 h1:AEK13rfgtiZJL2YsNK+W4ihhYCuukcRom8WPP/w/L54=
|
||||||
go.mau.fi/libsignal v0.2.2/go.mod h1:CRlIQg2J8uYTfDFvNoO8/KcZjs5cey0vbc6oj/bssY0=
|
go.mau.fi/util v0.8.6/go.mod h1:uNB3UTXFbkpp7xL1M/WvQks90B/L4gvbLpbS0603KOE=
|
||||||
go.mau.fi/util v0.11.0 h1:JNEpkVRxfTdJUC0DIGKiRlLKzl4G/qLX/eVzoP7QuwY=
|
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
|
||||||
go.mau.fi/util v0.11.0/go.mod h1:04AEFVsc4zNdIkDC1gkdqbg/nZFolHY50pyXh2DSy7s=
|
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
|
||||||
go.mau.fi/webp v0.3.0 h1:gVHQZtz21Ziwj+CDuklbX9mqpsnDIFKxs/BJyV7iZzA=
|
go.mau.fi/whatsmeow v0.0.0-20250411192951-5ab78fadbf91 h1:eziiUdmaGyhM7Fxn4IMRClBrX1b2sCAFGSBE2Z57yNg=
|
||||||
go.mau.fi/webp v0.3.0/go.mod h1:rlZFTev+dYxhvk+XNBP/5GcTt4gXmzAB4DU0aGUYIQo=
|
go.mau.fi/whatsmeow v0.0.0-20250411192951-5ab78fadbf91/go.mod h1:aj6MpceCuJ6m712lPEMAh8iAw2TkABx6TnPjMcdG0eE=
|
||||||
go.mau.fi/whatsmeow v0.0.0-20261026125351-1fbae988179a h1:ZyCV/mWfRkutxs5R0K3v+oyTbF4huprSlPZg/LEFQ7k=
|
go.mau.fi/zeroconfig v0.1.3 h1:As9wYDKmktjmNZW5i1vn8zvJlmGKHeVxHVIBMXsm4kM=
|
||||||
go.mau.fi/whatsmeow v0.0.0-20261026125351-1fbae988179a/go.mod h1:iY4lm61nTLWsNiIE1EmHUZwhKGBeoV4dhnVE2uMtNoQ=
|
go.mau.fi/zeroconfig v0.1.3/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70=
|
||||||
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
|
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||||
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
|
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||||
golang.org/x/crypto v0.58.0 h1:COkYLr4k7nDI4r6QtMML2AfzeIswWHAJthLS6K/M/54=
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||||
golang.org/x/crypto v0.58.0/go.mod h1:Xh+kl5A+M0gMiWDytH2Hhr5Z7WbNCpv/eB/EFgfSk+w=
|
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||||
golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e h1:HgI6W1OolKK6tcV74is+rl9SMT0njsHlp0TbQiGXI/A=
|
golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
|
||||||
golang.org/x/exp v0.0.0-20261014143641-f0251eb4aa3e/go.mod h1:DBoxwWhjaW8DJJw/YeM4zpeOKMC1FMXSblsYy+1mFSw=
|
golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
|
||||||
golang.org/x/image v0.47.0 h1:za/8gVz++PPZqmZJBNSISICN5Rrj5zixP/8K++fgmqo=
|
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||||
golang.org/x/image v0.47.0/go.mod h1:IkN5o4A7vq55vXN8FWjjnhg9Rs2GOtffqsSi9mWAKYE=
|
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||||
golang.org/x/mod v0.42.0 h1:ICeL3t2L30ljSPHHxDnR4fqVcBt/zR2zJAb9juavmQg=
|
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||||
golang.org/x/mod v0.42.0/go.mod h1:u5fzIrRInkL8MaO1cbRkUHT64v303YIT5KEqpNr7jRs=
|
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||||
golang.org/x/net v0.61.0 h1:RR+6j/BTBCrggOLugxa+66R8FvoN6MHlzDlZIbN9YFw=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
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/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.49.0 h1:XbzkgYJHdqh/8m2Uu0W/dQv8nktxx4BFHp1M0gROTXA=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.49.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og=
|
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||||
golang.org/x/text v0.43.0 h1:1QivrlhwAsnMOcqgOdl9my7s9OyZl8O5/UGUTJVXoWc=
|
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
golang.org/x/text v0.43.0/go.mod h1:hbSIYA/amXcRXTFSZqh8tWHEoqRvvbKPJ2uz/KU+a+s=
|
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||||
google.golang.org/protobuf v1.36.12 h1:pJOKDDOyeXErUroCihFAd5LQuwXBSpVnKGrj5o/fwxc=
|
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||||
google.golang.org/protobuf v1.36.12/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY=
|
||||||
|
google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||||
|
|
@ -103,5 +101,5 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
|
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
|
||||||
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
||||||
maunium.net/go/mautrix v0.32.1-0.20261105141340-842796acf445 h1:ZI9Y2YEmjqQDloOCiciHeynTRYjUwK0ZZbNcRH/5NOc=
|
maunium.net/go/mautrix v0.23.3-0.20250405234116-e675a3c09c38 h1:fIe2+kYndm3Mm/DwQ4FsODk2DjrLeEeW7tKtZjyERqM=
|
||||||
maunium.net/go/mautrix v0.32.1-0.20261105141340-842796acf445/go.mod h1:tZSwPzw+/0pa0dxGufkh0uJqM84BNxB6e2I0+S0UyjU=
|
maunium.net/go/mautrix v0.23.3-0.20250405234116-e675a3c09c38/go.mod h1:pCYLHmo02Jauak/9VlTkbGPrBMvLXsGqTGMNOx+L2PE=
|
||||||
|
|
|
||||||
|
|
@ -28,9 +28,6 @@ import (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (wa *WhatsAppClient) obfuscateJID(jid types.JID) string {
|
func (wa *WhatsAppClient) obfuscateJID(jid types.JID) string {
|
||||||
if jid.Server == types.HiddenUserServer {
|
|
||||||
return jid.String()
|
|
||||||
}
|
|
||||||
// Turn the first 4 bytes of HMAC-SHA256(user_mxid, phone) into a number and replace the middle of the actual phone with that deterministic random number.
|
// Turn the first 4 bytes of HMAC-SHA256(user_mxid, phone) into a number and replace the middle of the actual phone with that deterministic random number.
|
||||||
randomNumber := binary.BigEndian.Uint32(hmac.New(sha256.New, []byte(wa.UserLogin.UserMXID)).Sum([]byte(jid.User))[:4])
|
randomNumber := binary.BigEndian.Uint32(hmac.New(sha256.New, []byte(wa.UserLogin.UserMXID)).Sum([]byte(jid.User))[:4])
|
||||||
return fmt.Sprintf("+%s-%d-%s:%d", jid.User[:1], randomNumber, jid.User[len(jid.User)-2:], jid.Device)
|
return fmt.Sprintf("+%s-%d-%s:%d", jid.User[:1], randomNumber, jid.User[len(jid.User)-2:], jid.Device)
|
||||||
|
|
|
||||||
|
|
@ -4,17 +4,13 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"runtime/debug"
|
|
||||||
"slices"
|
"slices"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/rs/zerolog"
|
"github.com/rs/zerolog"
|
||||||
"go.mau.fi/util/exerrors"
|
|
||||||
"go.mau.fi/util/exmaps"
|
|
||||||
"go.mau.fi/util/ptr"
|
"go.mau.fi/util/ptr"
|
||||||
"go.mau.fi/whatsmeow"
|
"go.mau.fi/whatsmeow"
|
||||||
"go.mau.fi/whatsmeow/proto/waE2E"
|
"go.mau.fi/whatsmeow/proto/waE2E"
|
||||||
|
|
@ -32,176 +28,35 @@ import (
|
||||||
|
|
||||||
var _ bridgev2.BackfillingNetworkAPI = (*WhatsAppClient)(nil)
|
var _ bridgev2.BackfillingNetworkAPI = (*WhatsAppClient)(nil)
|
||||||
|
|
||||||
func (wa *WhatsAppClient) historySyncLoop(ctx context.Context) {
|
const historySyncDispatchWait = 30 * time.Second
|
||||||
dispatchTimer := time.NewTimer(wa.Main.Config.HistorySync.DispatchWait)
|
|
||||||
|
func (wa *WhatsAppClient) historySyncLoop(ctx context.Context) {
|
||||||
|
dispatchTimer := time.NewTimer(historySyncDispatchWait)
|
||||||
|
|
||||||
var timerPending atomic.Bool
|
|
||||||
if !wa.isNewLogin && wa.UserLogin.Metadata.(*waid.UserLoginMetadata).HistorySyncPortalsNeedCreating {
|
if !wa.isNewLogin && wa.UserLogin.Metadata.(*waid.UserLoginMetadata).HistorySyncPortalsNeedCreating {
|
||||||
dispatchTimer.Reset(5 * time.Second)
|
dispatchTimer.Reset(5 * time.Second)
|
||||||
timerPending.Store(true)
|
|
||||||
} else {
|
} else {
|
||||||
dispatchTimer.Stop()
|
dispatchTimer.Stop()
|
||||||
}
|
}
|
||||||
if wa.Client.ManualHistorySyncDownload {
|
wa.UserLogin.Log.Debug().Msg("Starting history sync loop")
|
||||||
// Wake up the queue once to check if there are pending notifications
|
|
||||||
select {
|
|
||||||
case wa.historySyncWakeup <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
wa.UserLogin.Log.Debug().Msg("Starting history sync loops")
|
|
||||||
// Separate loop for creating portals to ensure it doesn't block processing new history sync payloads.
|
|
||||||
go func() {
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
case evt := <-wa.historySyncs:
|
||||||
|
dispatchTimer.Stop()
|
||||||
|
wa.handleWAHistorySync(ctx, evt)
|
||||||
|
dispatchTimer.Reset(historySyncDispatchWait)
|
||||||
case <-dispatchTimer.C:
|
case <-dispatchTimer.C:
|
||||||
timerPending.Store(false)
|
|
||||||
wa.createPortalsFromHistorySync(ctx)
|
wa.createPortalsFromHistorySync(ctx)
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
wa.UserLogin.Log.Debug().Msg("Stopping portal creation history sync loop")
|
wa.UserLogin.Log.Debug().Msg("Stopping history sync loop")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}()
|
|
||||||
for {
|
|
||||||
var resetTimer, errored bool
|
|
||||||
select {
|
|
||||||
case <-wa.historySyncWakeup:
|
|
||||||
dispatchTimer.Stop()
|
|
||||||
notif, rowid, err := wa.Main.DB.HSNotif.GetNext(ctx, wa.UserLogin.ID)
|
|
||||||
if err != nil {
|
|
||||||
wa.UserLogin.Log.Err(err).Msg("Failed to get next history sync notification")
|
|
||||||
} else if notif == nil {
|
|
||||||
wa.UserLogin.Log.Debug().Msg("No more queued history sync notifications")
|
|
||||||
} else {
|
|
||||||
resetTimer, errored = 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")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if resetTimer {
|
|
||||||
timerPending.Store(true)
|
|
||||||
}
|
|
||||||
if timerPending.Load() {
|
|
||||||
dispatchTimer.Reset(wa.Main.Config.HistorySync.DispatchWait)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wa *WhatsAppClient) saveWAHistorySyncNotification(ctx context.Context, evt *waE2E.HistorySyncNotification) {
|
func (wa *WhatsAppClient) handleWAHistorySync(ctx context.Context, evt *waHistorySync.HistorySync) {
|
||||||
err := wa.Main.DB.HSNotif.Put(ctx, wa.UserLogin.ID, evt)
|
|
||||||
if err != nil {
|
|
||||||
wa.UserLogin.Log.Err(err).Msg("Failed to store history sync notification in queue")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
wa.UserLogin.Log.Debug().
|
|
||||||
Stringer("sync_type", evt.GetSyncType()).
|
|
||||||
Uint32("chunk_order", evt.GetChunkOrder()).
|
|
||||||
Uint32("progress", evt.GetProgress()).
|
|
||||||
Msg("Stored history sync notification in queue")
|
|
||||||
select {
|
|
||||||
case wa.historySyncWakeup <- struct{}{}:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wa *WhatsAppClient) downloadAndSaveWAHistorySyncData(ctx context.Context, evt *waE2E.HistorySyncNotification, rowid int) (resetTimer, errored 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")
|
|
||||||
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 {
|
|
||||||
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)
|
|
||||||
if innerErr != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
innerErr = wa.Main.DB.HSNotif.Delete(ctx, rowid)
|
|
||||||
if innerErr != nil {
|
|
||||||
innerErr = fmt.Errorf("failed to delete queued history sync notification: %w", innerErr)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
})
|
|
||||||
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 {
|
|
||||||
if evt == nil || evt.SyncType == nil {
|
if evt == nil || evt.SyncType == nil {
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
log := wa.UserLogin.Log.With().
|
log := wa.UserLogin.Log.With().
|
||||||
Str("action", "store history sync").
|
Str("action", "store history sync").
|
||||||
|
|
@ -213,12 +68,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
||||||
if evt.GetGlobalSettings() != nil {
|
if evt.GetGlobalSettings() != nil {
|
||||||
log.Debug().Interface("global_settings", evt.GetGlobalSettings()).Msg("Got global settings in history sync")
|
log.Debug().Interface("global_settings", evt.GetGlobalSettings()).Msg("Got global settings in history sync")
|
||||||
}
|
}
|
||||||
if evt.GetSyncType() == waHistorySync.HistorySync_INITIAL_STATUS_V3 ||
|
if evt.GetSyncType() == waHistorySync.HistorySync_INITIAL_STATUS_V3 || evt.GetSyncType() == waHistorySync.HistorySync_PUSH_NAME || evt.GetSyncType() == waHistorySync.HistorySync_NON_BLOCKING_DATA {
|
||||||
evt.GetSyncType() == waHistorySync.HistorySync_PUSH_NAME ||
|
|
||||||
evt.GetSyncType() == waHistorySync.HistorySync_NON_BLOCKING_DATA {
|
|
||||||
if evt.GetSyncType() == waHistorySync.HistorySync_PUSH_NAME {
|
|
||||||
wa.pushNamesSynced.Set()
|
|
||||||
}
|
|
||||||
log.Debug().
|
log.Debug().
|
||||||
Int("conversation_count", len(evt.GetConversations())).
|
Int("conversation_count", len(evt.GetConversations())).
|
||||||
Int("pushname_count", len(evt.GetPushnames())).
|
Int("pushname_count", len(evt.GetPushnames())).
|
||||||
|
|
@ -226,71 +76,40 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
||||||
Int("recent_sticker_count", len(evt.GetRecentStickers())).
|
Int("recent_sticker_count", len(evt.GetRecentStickers())).
|
||||||
Int("past_participant_count", len(evt.GetPastParticipants())).
|
Int("past_participant_count", len(evt.GetPastParticipants())).
|
||||||
Msg("Ignoring history sync")
|
Msg("Ignoring history sync")
|
||||||
return nil
|
return
|
||||||
}
|
}
|
||||||
log.Info().
|
log.Info().
|
||||||
Int("conversation_count", len(evt.GetConversations())).
|
Int("conversation_count", len(evt.GetConversations())).
|
||||||
Int("past_participant_count", len(evt.GetPastParticipants())).
|
Int("past_participant_count", len(evt.GetPastParticipants())).
|
||||||
Dict("notification_metadata", zerolog.Dict().
|
|
||||||
Int64("oldest_msg_in_chunk_ts", notif.GetOldestMsgInChunkTimestampSec()).
|
|
||||||
Any("full_request_meta", notif.GetFullHistorySyncOnDemandRequestMetadata()).
|
|
||||||
Any("access_status", notif.GetMessageAccessStatus()).
|
|
||||||
Str("peer_data_request_session_id", notif.GetPeerDataRequestSessionID())).
|
|
||||||
Msg("Storing history sync")
|
Msg("Storing history sync")
|
||||||
start := time.Now()
|
|
||||||
successfullySavedTotal := 0
|
successfullySavedTotal := 0
|
||||||
failedToSaveTotal := 0
|
failedToSaveTotal := 0
|
||||||
totalMessageCount := 0
|
totalMessageCount := 0
|
||||||
for _, conv := range evt.GetConversations() {
|
for _, conv := range evt.GetConversations() {
|
||||||
log := log.With().
|
|
||||||
Int("msg_count", len(conv.GetMessages())).
|
|
||||||
Logger()
|
|
||||||
jid, err := types.ParseJID(conv.GetID())
|
jid, err := types.ParseJID(conv.GetID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
totalMessageCount += len(conv.GetMessages())
|
totalMessageCount += len(conv.GetMessages())
|
||||||
log.Warn().Err(err).
|
log.Warn().Err(err).
|
||||||
Str("chat_jid", conv.GetID()).
|
Str("chat_jid", conv.GetID()).
|
||||||
|
Int("msg_count", len(conv.GetMessages())).
|
||||||
Msg("Failed to parse chat JID in history sync")
|
Msg("Failed to parse chat JID in history sync")
|
||||||
continue
|
continue
|
||||||
} else if jid.Server == types.BroadcastServer {
|
} else if jid.Server == types.BroadcastServer {
|
||||||
log.Debug().Stringer("chat_jid", jid).Msg("Skipping broadcast list in history sync")
|
log.Debug().Stringer("chat_jid", jid).Msg("Skipping broadcast list in history sync")
|
||||||
continue
|
continue
|
||||||
} else if jid == types.PSAJID {
|
}
|
||||||
// New messages in the official updates chat aren't bridged because they use a different path,
|
|
||||||
// so drop history as well. This should be removed if the PSA chat gets properly bridged in the future.
|
|
||||||
log.Debug().Stringer("chat_jid", jid).Msg("Skipping PSA in history sync")
|
|
||||||
continue
|
|
||||||
} else {
|
|
||||||
totalMessageCount += len(conv.GetMessages())
|
totalMessageCount += len(conv.GetMessages())
|
||||||
}
|
log := log.With().
|
||||||
if jid == types.MetaAIJID {
|
Stringer("chat_jid", jid).
|
||||||
jid = types.NewMetaAIJID
|
Int("msg_count", len(conv.GetMessages())).
|
||||||
} else if jid.Server == types.DefaultUserServer {
|
Logger()
|
||||||
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)
|
|
||||||
})
|
|
||||||
|
|
||||||
var firstItemTime, lastItemTime time.Time
|
var minTime, maxTime time.Time
|
||||||
|
var minTimeIndex, maxTimeIndex int
|
||||||
|
|
||||||
ignoredTypes := 0
|
ignoredTypes := 0
|
||||||
rawMessages := conv.GetMessages()
|
messages := make([]*wadb.HistorySyncMessageTuple, 0, len(conv.GetMessages()))
|
||||||
messages := make([]*wadb.HistorySyncMessageTuple, 0, len(rawMessages))
|
for i, rawMsg := range conv.GetMessages() {
|
||||||
allowClamp := conv.GetCommentsCount() == 0
|
|
||||||
var newerTS uint64
|
|
||||||
for i, rawMsg := range rawMessages {
|
|
||||||
// Don't store messages that will just be skipped.
|
// Don't store messages that will just be skipped.
|
||||||
msgEvt, err := wa.Client.ParseWebMessage(jid, rawMsg.GetMessage())
|
msgEvt, err := wa.Client.ParseWebMessage(jid, rawMsg.GetMessage())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -298,43 +117,23 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
||||||
Int("msg_index", i).
|
Int("msg_index", i).
|
||||||
Str("msg_id", rawMsg.GetMessage().GetKey().GetID()).
|
Str("msg_id", rawMsg.GetMessage().GetKey().GetID()).
|
||||||
Uint64("msg_time_seconds", rawMsg.GetMessage().GetMessageTimestamp()).
|
Uint64("msg_time_seconds", rawMsg.GetMessage().GetMessageTimestamp()).
|
||||||
Stringer("stub_type", rawMsg.GetMessage().GetMessageStubType()).
|
|
||||||
Str("message_type", getMessageType(rawMsg.GetMessage().GetMessage())).
|
|
||||||
Bool("from_me", rawMsg.GetMessage().GetKey().GetFromMe()).
|
|
||||||
Msg("Dropping historical message due to parse error")
|
Msg("Dropping historical message due to parse error")
|
||||||
log.Trace().
|
|
||||||
Any("web_message_info", rawMsg.GetMessage()).
|
|
||||||
Msg("Content of event that failed to parse")
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if firstItemTime.IsZero() {
|
if minTime.IsZero() || msgEvt.Info.Timestamp.Before(minTime) {
|
||||||
firstItemTime = msgEvt.Info.Timestamp
|
minTime = msgEvt.Info.Timestamp
|
||||||
|
minTimeIndex = i
|
||||||
|
}
|
||||||
|
if maxTime.IsZero() || msgEvt.Info.Timestamp.After(maxTime) {
|
||||||
|
maxTime = msgEvt.Info.Timestamp
|
||||||
|
maxTimeIndex = i
|
||||||
}
|
}
|
||||||
lastItemTime = msgEvt.Info.Timestamp
|
|
||||||
|
|
||||||
msgType := getMessageType(msgEvt.Message)
|
msgType := getMessageType(msgEvt.Message)
|
||||||
if msgType == "ignore" || strings.HasPrefix(msgType, "unknown_protocol_") {
|
if msgType == "ignore" || strings.HasPrefix(msgType, "unknown_protocol_") {
|
||||||
ignoredTypes++
|
ignoredTypes++
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
// Comments (replies) in announcement groups are not ordered by timestamp, so don't clamp them.
|
|
||||||
if rawMsg.GetMessage().GetCommentMetadata().GetCommentParentKey() != nil {
|
|
||||||
allowClamp = false
|
|
||||||
}
|
|
||||||
// WhatsApp has bugs where some random messages will have timestamps decades in the future.
|
|
||||||
// To ensure they don't mess up our ordering, require timestamps of older messages to be
|
|
||||||
// before the previous (newer) message.
|
|
||||||
if currentTS := rawMsg.GetMessage().GetMessageTimestamp(); newerTS > 0 && allowClamp && currentTS > newerTS {
|
|
||||||
log.Warn().
|
|
||||||
Time("current_ts", time.Unix(int64(currentTS), 0)).
|
|
||||||
Time("prev_ts", time.Unix(int64(newerTS), 0)).
|
|
||||||
Int("msg_index", i).
|
|
||||||
Str("msg_id", rawMsg.GetMessage().GetKey().GetID()).
|
|
||||||
Msg("Clamping message timestamp")
|
|
||||||
rawMsg.Message.MessageTimestamp = ptr.Ptr(newerTS)
|
|
||||||
msgEvt.Info.Timestamp = time.Unix(int64(newerTS), 0)
|
|
||||||
}
|
|
||||||
newerTS = rawMsg.GetMessage().GetMessageTimestamp()
|
|
||||||
marshaled, err := proto.Marshal(rawMsg)
|
marshaled, err := proto.Marshal(rawMsg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warn().Err(err).
|
log.Warn().Err(err).
|
||||||
|
|
@ -349,83 +148,56 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
||||||
log.Debug().
|
log.Debug().
|
||||||
Int("wrapped_count", len(messages)).
|
Int("wrapped_count", len(messages)).
|
||||||
Int("ignored_msg_type_count", ignoredTypes).
|
Int("ignored_msg_type_count", ignoredTypes).
|
||||||
Time("first_item_time", firstItemTime).
|
Time("lowest_time", minTime).
|
||||||
Time("last_item_time", lastItemTime).
|
Int("lowest_time_index", minTimeIndex).
|
||||||
|
Time("highest_time", maxTime).
|
||||||
|
Int("highest_time_index", maxTimeIndex).
|
||||||
Dict("metadata", zerolog.Dict().
|
Dict("metadata", zerolog.Dict().
|
||||||
Uint32("ephemeral_expiration", conv.GetEphemeralExpiration()).
|
Uint32("ephemeral_expiration", conv.GetEphemeralExpiration()).
|
||||||
Int64("ephemeral_setting_timestamp", conv.GetEphemeralSettingTimestamp()).
|
Int64("ephemeral_setting_timestamp", conv.GetEphemeralSettingTimestamp()).
|
||||||
Uint64("last_message_ts", conv.GetLastMsgTimestamp()).
|
|
||||||
Bool("marked_unread", conv.GetMarkedAsUnread()).
|
Bool("marked_unread", conv.GetMarkedAsUnread()).
|
||||||
Bool("archived", conv.GetArchived()).
|
Bool("archived", conv.GetArchived()).
|
||||||
Uint32("pinned", conv.GetPinned()).
|
Uint32("pinned", conv.GetPinned()).
|
||||||
Uint64("mute_end", conv.GetMuteEndTime()).
|
Uint64("mute_end", conv.GetMuteEndTime()).
|
||||||
Uint32("unread_count", conv.GetUnreadCount()).
|
Uint32("unread_count", conv.GetUnreadCount()),
|
||||||
Bool("end_of_history", conv.GetEndOfHistoryTransfer()).
|
|
||||||
Stringer("end_of_history_type", conv.GetEndOfHistoryTransferType()),
|
|
||||||
).
|
).
|
||||||
Msg("Collected messages to save from history sync conversation")
|
Msg("Collected messages to save from history sync conversation")
|
||||||
|
|
||||||
if len(messages) > 0 {
|
if len(messages) > 0 {
|
||||||
err = wa.Main.DB.Conversation.Put(ctx, wadb.NewConversation(wa.UserLogin.ID, jid, conv, firstItemTime))
|
err = wa.Main.DB.Conversation.Put(ctx, wadb.NewConversation(wa.UserLogin.ID, jid, conv))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if stopOnError {
|
|
||||||
return fmt.Errorf("failed to save conversation metadata for %s: %w", jid, err)
|
|
||||||
}
|
|
||||||
log.Err(err).Msg("Failed to save conversation metadata")
|
log.Err(err).Msg("Failed to save conversation metadata")
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
err = wa.Main.DB.Message.Put(ctx, wa.UserLogin.ID, jid, messages)
|
err = wa.Main.DB.Message.Put(ctx, wa.UserLogin.ID, jid, messages)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if stopOnError {
|
|
||||||
return fmt.Errorf("failed to save messages in %s: %w", jid, err)
|
|
||||||
}
|
|
||||||
log.Err(err).Msg("Failed to save messages")
|
log.Err(err).Msg("Failed to save messages")
|
||||||
failedToSaveTotal += len(messages)
|
failedToSaveTotal += len(messages)
|
||||||
} else {
|
} else {
|
||||||
successfullySavedTotal += len(messages)
|
successfullySavedTotal += len(messages)
|
||||||
}
|
}
|
||||||
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)
|
|
||||||
}
|
|
||||||
log.Err(err).Msg("Failed to mark backfill task as not done")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
log.Info().
|
log.Info().
|
||||||
Int("total_saved_count", successfullySavedTotal).
|
Int("total_saved_count", successfullySavedTotal).
|
||||||
Int("total_failed_count", failedToSaveTotal).
|
Int("total_failed_count", failedToSaveTotal).
|
||||||
Int("total_message_count", totalMessageCount).
|
Int("total_message_count", totalMessageCount).
|
||||||
Dur("duration", time.Since(start)).
|
|
||||||
Msg("Finished storing history sync")
|
Msg("Finished storing history sync")
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) {
|
func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) {
|
||||||
log := wa.UserLogin.Log.With().
|
log := wa.UserLogin.Log.With().
|
||||||
Str("action", "create portals from history sync").
|
Str("action", "create portals from history sync").
|
||||||
Logger()
|
Logger()
|
||||||
defer func() {
|
|
||||||
if v := recover(); v != nil {
|
|
||||||
log.Err(exerrors.RecoverToError(v)).
|
|
||||||
Bytes(zerolog.ErrorStackFieldName, debug.Stack()).
|
|
||||||
Msg("History sync portal creation panicked")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
ctx = log.WithContext(ctx)
|
ctx = log.WithContext(ctx)
|
||||||
limit := wa.Main.Config.HistorySync.MaxInitialConversations
|
limit := wa.Main.Config.HistorySync.MaxInitialConversations
|
||||||
loginTS := wa.UserLogin.Metadata.(*waid.UserLoginMetadata).LoggedInAt
|
log.Info().Int("limit", limit).Msg("Creating portals from history sync")
|
||||||
conversations, err := wa.Main.DB.Conversation.GetRecent(ctx, wa.UserLogin.ID, limit, loginTS)
|
conversations, err := wa.Main.DB.Conversation.GetRecent(ctx, wa.UserLogin.ID, limit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Err(err).Msg("Failed to get recent conversations from database")
|
log.Err(err).Msg("Failed to get recent conversations from database")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Info().
|
log.Info().Int("conversation_count", len(conversations)).Msg("Creating portals from history sync")
|
||||||
Int("limit", limit).
|
|
||||||
Int("conversation_count", len(conversations)).
|
|
||||||
Int64("login_timestamp", loginTS.Unix()).
|
|
||||||
Msg("Creating portals from history sync")
|
|
||||||
rateLimitErrors := 0
|
rateLimitErrors := 0
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(len(conversations))
|
wg.Add(len(conversations))
|
||||||
|
|
@ -441,21 +213,12 @@ func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) {
|
||||||
if conv.ChatJID == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast {
|
if conv.ChatJID == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast {
|
||||||
wg.Done()
|
wg.Done()
|
||||||
continue
|
continue
|
||||||
} else if conv.ChatJID == types.PSAJID || conv.ChatJID == types.LegacyPSAJID {
|
|
||||||
// We don't currently support new PSAs, so don't bother backfilling them either
|
|
||||||
wg.Done()
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
// TODO can the chat info fetch be avoided entirely?
|
// TODO can the chat info fetch be avoided entirely?
|
||||||
select {
|
time.Sleep(time.Duration(rateLimitErrors) * time.Second)
|
||||||
case <-time.After(time.Duration(rateLimitErrors) * time.Second):
|
wrappedInfo, err := wa.getChatInfo(ctx, conv.ChatJID, conv)
|
||||||
case <-ctx.Done():
|
if errors.Is(err, whatsmeow.ErrNotInGroup) {
|
||||||
log.Warn().Err(ctx.Err()).Msg("Context cancelled, stopping history sync portal creation")
|
log.Debug().Stringer("chat_jid", conv.ChatJID).
|
||||||
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).
|
|
||||||
Msg("Skipping creating room because the user is not a participant")
|
Msg("Skipping creating room because the user is not a participant")
|
||||||
//err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, conv.ChatJID)
|
//err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, conv.ChatJID)
|
||||||
//if err != nil {
|
//if err != nil {
|
||||||
|
|
@ -473,30 +236,21 @@ func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) {
|
||||||
log.Err(err).Stringer("chat_jid", conv.ChatJID).
|
log.Err(err).Stringer("chat_jid", conv.ChatJID).
|
||||||
Int("error_count", rateLimitErrors).
|
Int("error_count", rateLimitErrors).
|
||||||
Msg("Ratelimit error getting chat info, retrying after sleep")
|
Msg("Ratelimit error getting chat info, retrying after sleep")
|
||||||
select {
|
time.Sleep(time.Duration(rateLimitErrors) * time.Minute)
|
||||||
case <-time.After(time.Duration(rateLimitErrors) * time.Second):
|
|
||||||
case <-ctx.Done():
|
|
||||||
log.Warn().Err(ctx.Err()).Msg("Context cancelled, stopping history sync portal creation")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
continue
|
continue
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
log.Err(err).Stringer("chat_jid", conv.ChatJID).Msg("Failed to get chat info")
|
log.Err(err).Stringer("chat_jid", conv.ChatJID).Msg("Failed to get chat info")
|
||||||
wg.Done()
|
wg.Done()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
res := wa.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{
|
wa.Main.Bridge.QueueRemoteEvent(wa.UserLogin, &simplevent.ChatResync{
|
||||||
EventMeta: simplevent.EventMeta{
|
EventMeta: simplevent.EventMeta{
|
||||||
Type: bridgev2.RemoteEventChatResync,
|
Type: bridgev2.RemoteEventChatResync,
|
||||||
LogContext: func(c zerolog.Context) zerolog.Context {
|
LogContext: nil,
|
||||||
return c.
|
|
||||||
Stringer("chat_jid", conv.ChatJID).
|
|
||||||
Time("latest_message_ts", conv.LastMessageTimestamp)
|
|
||||||
},
|
|
||||||
PortalKey: wa.makeWAPortalKey(conv.ChatJID),
|
PortalKey: wa.makeWAPortalKey(conv.ChatJID),
|
||||||
CreatePortal: true,
|
CreatePortal: true,
|
||||||
PostHandleFunc: func(ctx context.Context, portal *bridgev2.Portal) {
|
PostHandleFunc: func(ctx context.Context, portal *bridgev2.Portal) {
|
||||||
err := wa.Main.DB.Conversation.MarkSynced(ctx, wa.UserLogin.ID, conv.ChatJID, loginTS)
|
err := wa.Main.DB.Conversation.MarkBridged(ctx, wa.UserLogin.ID, conv.ChatJID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to mark conversation as bridged")
|
zerolog.Ctx(ctx).Err(err).Msg("Failed to mark conversation as bridged")
|
||||||
}
|
}
|
||||||
|
|
@ -506,10 +260,6 @@ func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) {
|
||||||
ChatInfo: wrappedInfo,
|
ChatInfo: wrappedInfo,
|
||||||
LatestMessageTS: conv.LastMessageTimestamp,
|
LatestMessageTS: conv.LastMessageTimestamp,
|
||||||
})
|
})
|
||||||
if !res.Success {
|
|
||||||
log.Debug().Msg("Cancelling history sync portal creation loop")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
log.Info().Int("conversation_count", len(conversations)).Msg("Finished creating portals from history sync")
|
log.Info().Int("conversation_count", len(conversations)).Msg("Finished creating portals from history sync")
|
||||||
go func() {
|
go func() {
|
||||||
|
|
@ -528,73 +278,40 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if portalJID.Server == types.DefaultUserServer {
|
|
||||||
zerolog.Ctx(ctx).Warn().Stringer("portal_jid", portalJID).Msg("FetchMessages called for phone number portal")
|
|
||||||
}
|
|
||||||
var markRead bool
|
var markRead bool
|
||||||
var startTime, endTime *time.Time
|
var startTime, endTime *time.Time
|
||||||
var conv *wadb.Conversation
|
|
||||||
if params.Forward || wa.Main.Config.HistorySync.BackwardsOnDemand {
|
|
||||||
conv, err = wa.Main.DB.Conversation.Get(ctx, wa.UserLogin.ID, portalJID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get conversation from database: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if params.Forward {
|
if params.Forward {
|
||||||
if params.AnchorMessage != nil {
|
if params.AnchorMessage != nil {
|
||||||
startTime = ptr.Ptr(params.AnchorMessage.Timestamp)
|
startTime = ptr.Ptr(params.AnchorMessage.Timestamp)
|
||||||
}
|
}
|
||||||
if conv != nil {
|
conv, err := wa.Main.DB.Conversation.Get(ctx, wa.UserLogin.ID, portalJID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to get conversation from database: %w", err)
|
||||||
|
} else if conv != nil {
|
||||||
markRead = !ptr.Val(conv.MarkedAsUnread) && ptr.Val(conv.UnreadCount) == 0
|
markRead = !ptr.Val(conv.MarkedAsUnread) && ptr.Val(conv.UnreadCount) == 0
|
||||||
}
|
}
|
||||||
} else {
|
} else if params.Cursor != "" {
|
||||||
if params.AnchorMessage != nil {
|
|
||||||
endTime = ptr.Ptr(params.AnchorMessage.Timestamp)
|
|
||||||
}
|
|
||||||
if params.Cursor != "" {
|
|
||||||
endTimeUnix, err := strconv.ParseInt(string(params.Cursor), 10, 64)
|
endTimeUnix, err := strconv.ParseInt(string(params.Cursor), 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to parse cursor: %w", err)
|
return nil, fmt.Errorf("failed to parse cursor: %w", err)
|
||||||
}
|
}
|
||||||
cursorTime := time.Unix(endTimeUnix, 0)
|
endTime = ptr.Ptr(time.Unix(endTimeUnix, 0))
|
||||||
if endTime == nil || cursorTime.Before(*endTime) {
|
} else if params.AnchorMessage != nil {
|
||||||
endTime = &cursorTime
|
endTime = ptr.Ptr(params.AnchorMessage.Timestamp)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var anchorID types.MessageID
|
|
||||||
if params.AnchorMessage != nil {
|
|
||||||
parsedID, _ := waid.ParseMessageID(params.AnchorMessage.ID)
|
|
||||||
if parsedID != nil {
|
|
||||||
anchorID = parsedID.ID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var hasMore bool
|
|
||||||
if !params.Forward && wa.Main.Config.HistorySync.BackwardsOnDemand {
|
|
||||||
hasMore = conv != nil && ptr.Val(conv.EndOfHistoryTransferType) == waHistorySync.Conversation_COMPLETE_BUT_MORE_MESSAGES_REMAIN_ON_PRIMARY
|
|
||||||
}
|
}
|
||||||
messages, err := wa.Main.DB.Message.GetBetween(ctx, wa.UserLogin.ID, portalJID, startTime, endTime, params.Count+1)
|
messages, err := wa.Main.DB.Message.GetBetween(ctx, wa.UserLogin.ID, portalJID, startTime, endTime, params.Count+1)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to load messages from database: %w", err)
|
return nil, fmt.Errorf("failed to load messages from database: %w", err)
|
||||||
} else if len(messages) == 0 || (len(messages) == 1 && anchorID != "" && messages[0].GetKey().GetID() == anchorID) {
|
} else if len(messages) == 0 {
|
||||||
wa.deleteHistorySyncMessages(ctx, portalJID, 0, 0)
|
|
||||||
hasMore = hasMore && params.AnchorMessage != nil
|
|
||||||
if hasMore && !params.AllowSlowFetch {
|
|
||||||
return &bridgev2.FetchMessagesResponse{
|
|
||||||
MoreRequiresSlowFetch: true,
|
|
||||||
HasMore: true,
|
|
||||||
Forward: params.Forward,
|
|
||||||
}, nil
|
|
||||||
} else if hasMore {
|
|
||||||
return wa.fetchMessagesFromPhone(ctx, portalJID, params)
|
|
||||||
}
|
|
||||||
return &bridgev2.FetchMessagesResponse{
|
return &bridgev2.FetchMessagesResponse{
|
||||||
HasMore: false,
|
HasMore: false,
|
||||||
Forward: params.Forward,
|
Forward: params.Forward,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
if len(messages) > params.Count {
|
hasMore := false
|
||||||
oldestTS := messages[len(messages)-1].GetMessageTimestamp()
|
oldestTS := messages[len(messages)-1].GetMessageTimestamp()
|
||||||
|
newestTS := messages[0].GetMessageTimestamp()
|
||||||
|
if len(messages) > params.Count {
|
||||||
hasMore = true
|
hasMore = true
|
||||||
// For safety, cut off messages with the oldest timestamp in the response.
|
// For safety, cut off messages with the oldest timestamp in the response.
|
||||||
// Otherwise, if there are multiple messages with the same timestamp, the next fetch may miss some.
|
// Otherwise, if there are multiple messages with the same timestamp, the next fetch may miss some.
|
||||||
|
|
@ -605,118 +322,17 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
resp, err := wa.convertHistorySyncMessages(ctx, params.Portal, portalJID, messages, true, !params.Forward && !hasMore)
|
convertedMessages := make([]*bridgev2.BackfillMessage, len(messages))
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to convert messages: %w", err)
|
|
||||||
}
|
|
||||||
// TODO might be better to always set true here and let GetBetween return an empty result to mark as false?
|
|
||||||
resp.HasMore = hasMore
|
|
||||||
resp.Forward = params.Forward
|
|
||||||
resp.MarkRead = markRead
|
|
||||||
if params.Forward && !markRead && conv != nil && !ptr.Val(conv.MarkedAsUnread) {
|
|
||||||
resp.ReadUpTo = historySyncReadUpTo(resp.Messages, int(ptr.Val(conv.UnreadCount)))
|
|
||||||
}
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func historySyncReadUpTo(messages []*bridgev2.BackfillMessage, unreadCount int) time.Time {
|
|
||||||
if unreadCount <= 0 {
|
|
||||||
return time.Time{}
|
|
||||||
}
|
|
||||||
for i := len(messages) - 1; i >= 0; i-- {
|
|
||||||
if messages[i].Sender.IsFromMe {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
unreadCount--
|
|
||||||
if unreadCount == 0 {
|
|
||||||
return messages[i].Timestamp.Add(-time.Nanosecond)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return time.Time{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wa *WhatsAppClient) deleteHistorySyncMessages(ctx context.Context, portalJID types.JID, newestTS, oldestTS uint64) {
|
|
||||||
var err error
|
|
||||||
var rows int64
|
|
||||||
if (newestTS == 0 && oldestTS == 0) || !wa.Main.Bridge.Config.Backfill.Queue.AnyEnabled() {
|
|
||||||
// If the backfill queue isn't enabled, delete all messages after backfilling a batch.
|
|
||||||
rows, err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, portalJID)
|
|
||||||
} else {
|
|
||||||
// Otherwise just delete the messages that got backfilled
|
|
||||||
rows, err = wa.Main.DB.Message.DeleteBetween(ctx, wa.UserLogin.ID, portalJID, newestTS, oldestTS)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
zerolog.Ctx(ctx).Warn().Err(err).
|
|
||||||
Stringer("portal_jid", portalJID).
|
|
||||||
Uint64("newest_ts", newestTS).
|
|
||||||
Uint64("oldest_ts", oldestTS).
|
|
||||||
Msg("Failed to delete messages from database after backfill")
|
|
||||||
} else {
|
|
||||||
zerolog.Ctx(ctx).Debug().
|
|
||||||
Stringer("portal_jid", portalJID).
|
|
||||||
Uint64("newest_ts", newestTS).
|
|
||||||
Uint64("oldest_ts", oldestTS).
|
|
||||||
Int64("rows_affected", rows).
|
|
||||||
Msg("Deleted history sync messages from database")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wa *WhatsAppClient) convertHistorySyncMessages(
|
|
||||||
ctx context.Context,
|
|
||||||
portal *bridgev2.Portal,
|
|
||||||
portalJID types.JID,
|
|
||||||
messages []*waWeb.WebMessageInfo,
|
|
||||||
explodeOnError bool,
|
|
||||||
deleteEverything bool,
|
|
||||||
) (*bridgev2.FetchMessagesResponse, error) {
|
|
||||||
cli := wa.Client
|
|
||||||
if cli == nil {
|
|
||||||
return nil, bridgev2.ErrNotLoggedIn
|
|
||||||
}
|
|
||||||
oldestTS := messages[len(messages)-1].GetMessageTimestamp()
|
|
||||||
newestTS := messages[0].GetMessageTimestamp()
|
|
||||||
convertedMessages := make([]*bridgev2.BackfillMessage, 0, len(messages))
|
|
||||||
var mediaRequests []*wadb.MediaRequest
|
var mediaRequests []*wadb.MediaRequest
|
||||||
dups := make(exmaps.Set[networkid.MessageID])
|
|
||||||
for i, msg := range messages {
|
for i, msg := range messages {
|
||||||
evt, err := cli.ParseWebMessage(portalJID, msg)
|
evt, err := wa.Client.ParseWebMessage(portalJID, msg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if explodeOnError {
|
|
||||||
// This should never happen because the info is already parsed once before being stored in the database
|
// This should never happen because the info is already parsed once before being stored in the database
|
||||||
return nil, fmt.Errorf("failed to parse info of message %s: %w", msg.GetKey().GetID(), err)
|
return nil, fmt.Errorf("failed to parse info of message %s: %w", msg.GetKey().GetID(), err)
|
||||||
}
|
}
|
||||||
zerolog.Ctx(ctx).Warn().Err(err).
|
var mediaReq *wadb.MediaRequest
|
||||||
Int("msg_index", i).
|
|
||||||
Str("msg_id", msg.GetKey().GetID()).
|
|
||||||
Uint64("msg_time_seconds", msg.GetMessageTimestamp()).
|
|
||||||
Msg("Dropping historical message due to parse error")
|
|
||||||
zerolog.Ctx(ctx).Trace().
|
|
||||||
Any("web_message_info", msg.GetMessage()).
|
|
||||||
Msg("Content of event that failed to parse")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !explodeOnError {
|
|
||||||
msgType := getMessageType(evt.Message)
|
|
||||||
if msgType == "ignore" || strings.HasPrefix(msgType, "unknown_protocol_") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !wa.ensureAltJIDs(ctx, &evt.Info.MessageSource, false) {
|
|
||||||
return nil, fmt.Errorf("failed to ensure alt JIDs for message %s", evt.Info.ID)
|
|
||||||
}
|
|
||||||
if evt.Info.IsFromMe {
|
|
||||||
wa.addPreviousOwnLID(ctx, evt.Info.Sender)
|
|
||||||
}
|
|
||||||
isViewOnce := evt.IsViewOnce || evt.IsViewOnceV2 || evt.IsViewOnceV2Extension
|
isViewOnce := evt.IsViewOnce || evt.IsViewOnceV2 || evt.IsViewOnceV2Extension
|
||||||
converted, mediaReq := wa.convertHistorySyncMessage(
|
convertedMessages[i], mediaReq = wa.convertHistorySyncMessage(ctx, params.Portal, &evt.Info, evt.Message, isViewOnce, msg.Reactions)
|
||||||
ctx, cli, portal, &evt.Info, evt.Message, evt.RawMessage, isViewOnce, msg.Reactions,
|
|
||||||
)
|
|
||||||
// This is a hack to remove duplicates where the same message is inserted with both the LID and phone number sender
|
|
||||||
// TODO prevent those being inserted in the first place instead of hacking around it here
|
|
||||||
if !dups.Add(converted.ID) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
convertedMessages = append(convertedMessages, converted)
|
|
||||||
if mediaReq != nil {
|
if mediaReq != nil {
|
||||||
mediaRequests = append(mediaRequests, mediaReq)
|
mediaRequests = append(mediaRequests, mediaReq)
|
||||||
}
|
}
|
||||||
|
|
@ -725,11 +341,23 @@ func (wa *WhatsAppClient) convertHistorySyncMessages(
|
||||||
return &bridgev2.FetchMessagesResponse{
|
return &bridgev2.FetchMessagesResponse{
|
||||||
Messages: convertedMessages,
|
Messages: convertedMessages,
|
||||||
Cursor: networkid.PaginationCursor(strconv.FormatUint(oldestTS, 10)),
|
Cursor: networkid.PaginationCursor(strconv.FormatUint(oldestTS, 10)),
|
||||||
|
HasMore: hasMore,
|
||||||
|
Forward: endTime == nil,
|
||||||
|
MarkRead: markRead,
|
||||||
|
// TODO set remaining or total count
|
||||||
CompleteCallback: func() {
|
CompleteCallback: func() {
|
||||||
if deleteEverything {
|
// TODO this only deletes after backfilling. If there's no need for backfill after a relogin,
|
||||||
wa.deleteHistorySyncMessages(ctx, portalJID, 1<<62, oldestTS)
|
// the messages will be stuck in the database
|
||||||
|
var err error
|
||||||
|
if !wa.Main.Bridge.Config.Backfill.Queue.Enabled && !wa.Main.Bridge.Config.Backfill.WillPaginateManually {
|
||||||
|
// If the backfill queue isn't enabled, delete all messages after backfilling a batch.
|
||||||
|
err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, portalJID)
|
||||||
} else {
|
} else {
|
||||||
wa.deleteHistorySyncMessages(ctx, portalJID, newestTS, oldestTS)
|
// Otherwise just delete the messages that got backfilled
|
||||||
|
err = wa.Main.DB.Message.DeleteBetween(ctx, wa.UserLogin.ID, portalJID, newestTS, oldestTS)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to delete messages from database after backfill")
|
||||||
}
|
}
|
||||||
if len(mediaRequests) > 0 {
|
if len(mediaRequests) > 0 {
|
||||||
go func(ctx context.Context) {
|
go func(ctx context.Context) {
|
||||||
|
|
@ -748,201 +376,39 @@ func (wa *WhatsAppClient) convertHistorySyncMessages(
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, portalJID types.JID, params bridgev2.FetchMessagesParams) (*bridgev2.FetchMessagesResponse, error) {
|
|
||||||
if params.AnchorMessage == nil {
|
|
||||||
return nil, fmt.Errorf("anchor message is required to fetch messages from phone")
|
|
||||||
}
|
|
||||||
anchor := params.AnchorMessage
|
|
||||||
parsed, err := waid.ParseMessageID(anchor.ID)
|
|
||||||
if err != nil {
|
|
||||||
anchor, err = wa.Main.Bridge.DB.Message.GetFirstPortalMessageExcludingIDPattern(ctx, params.Portal.PortalKey, "fake:%")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to get first non-fake portal message: %w", err)
|
|
||||||
} else if anchor == nil {
|
|
||||||
return &bridgev2.FetchMessagesResponse{}, nil
|
|
||||||
}
|
|
||||||
parsed, err = waid.ParseMessageID(anchor.ID)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to parse anchor message ID: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
msgID := wa.Client.GenerateMessageID()
|
|
||||||
if !wa.startOnDemandHistorySyncRequest(ctx, portalJID, msgID) {
|
|
||||||
return &bridgev2.FetchMessagesResponse{
|
|
||||||
HasMore: true,
|
|
||||||
Pending: true,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
reqData := wa.Client.BuildHistorySyncRequest(&types.MessageInfo{
|
|
||||||
MessageSource: types.MessageSource{
|
|
||||||
Chat: portalJID,
|
|
||||||
Sender: parsed.Sender,
|
|
||||||
IsFromMe: wa.IsOwnJID(parsed.Sender),
|
|
||||||
IsGroup: parsed.Chat.Server == types.GroupServer,
|
|
||||||
},
|
|
||||||
ID: parsed.ID,
|
|
||||||
Timestamp: anchor.Timestamp,
|
|
||||||
}, 50)
|
|
||||||
zerolog.Ctx(ctx).Debug().
|
|
||||||
Str("request_msg_id", msgID).
|
|
||||||
Stringer("portal_jid", portalJID).
|
|
||||||
Any("anchor_msg_parsed", parsed).
|
|
||||||
Any("request_data", reqData).
|
|
||||||
Msg("Sending history sync request")
|
|
||||||
_, err = wa.Client.SendMessage(ctx, wa.JID.ToNonAD(), reqData, whatsmeow.SendRequestExtra{
|
|
||||||
ID: msgID,
|
|
||||||
Peer: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
wa.onDemandHistorySyncLock.Lock()
|
|
||||||
delete(wa.onDemandHistorySyncRequests, portalJID)
|
|
||||||
wa.onDemandHistorySyncLock.Unlock()
|
|
||||||
return nil, fmt.Errorf("failed to send history sync request: %w", err)
|
|
||||||
}
|
|
||||||
return &bridgev2.FetchMessagesResponse{
|
|
||||||
HasMore: true,
|
|
||||||
Pending: true,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type onDemandHistorySyncRequest struct {
|
|
||||||
ID types.MessageID
|
|
||||||
SentAt time.Time
|
|
||||||
}
|
|
||||||
|
|
||||||
const onDemandHistorySyncRequestTimeout = 1 * time.Hour
|
|
||||||
|
|
||||||
func (wa *WhatsAppClient) startOnDemandHistorySyncRequest(ctx context.Context, portalJID types.JID, msgID types.MessageID) bool {
|
|
||||||
wa.onDemandHistorySyncLock.Lock()
|
|
||||||
defer wa.onDemandHistorySyncLock.Unlock()
|
|
||||||
if req, inFlight := wa.onDemandHistorySyncRequests[portalJID]; inFlight && time.Since(req.SentAt) < onDemandHistorySyncRequestTimeout {
|
|
||||||
zerolog.Ctx(ctx).Debug().
|
|
||||||
Str("in_flight_request_msg_id", req.ID).
|
|
||||||
Stringer("portal_jid", portalJID).
|
|
||||||
Time("sent_at", req.SentAt).
|
|
||||||
Msg("Not sending history sync request, previous request is still in flight")
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
wa.onDemandHistorySyncRequests[portalJID] = onDemandHistorySyncRequest{ID: msgID, SentAt: time.Now()}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wa *WhatsAppClient) finishOnDemandHistorySyncRequest(sessionID string) {
|
|
||||||
wa.onDemandHistorySyncLock.Lock()
|
|
||||||
defer wa.onDemandHistorySyncLock.Unlock()
|
|
||||||
for chatJID, req := range wa.onDemandHistorySyncRequests {
|
|
||||||
if req.ID == sessionID {
|
|
||||||
delete(wa.onDemandHistorySyncRequests, chatJID)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wa *WhatsAppClient) handleOnDemandHistorySync(ctx context.Context, blob *waHistorySync.HistorySync) {
|
|
||||||
if len(blob.GetConversations()) > 1 {
|
|
||||||
zerolog.Ctx(ctx).Warn().
|
|
||||||
Int("conversation_count", len(blob.GetConversations())).
|
|
||||||
Msg("Received on-demand history sync with multiple conversations")
|
|
||||||
}
|
|
||||||
for _, conv := range blob.GetConversations() {
|
|
||||||
portalJID, err := types.ParseJID(conv.GetID())
|
|
||||||
if err != nil {
|
|
||||||
zerolog.Ctx(ctx).Err(err).Str("jid", conv.GetID()).Msg("Failed to parse portal JID")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if portalJID == types.MetaAIJID {
|
|
||||||
portalJID = types.NewMetaAIJID
|
|
||||||
} else if portalJID.Server == types.DefaultUserServer {
|
|
||||||
lid, err := wa.GetStore().LIDs.GetLIDForPN(ctx, portalJID)
|
|
||||||
if err != nil {
|
|
||||||
zerolog.Ctx(ctx).Err(err).Stringer("lid", portalJID).Msg("Failed to get LID for phone number in on-demand history sync")
|
|
||||||
} else if lid.IsEmpty() {
|
|
||||||
zerolog.Ctx(ctx).Warn().Stringer("lid", portalJID).Msg("No LID found for phone number in on-demand history sync")
|
|
||||||
} else {
|
|
||||||
zerolog.Ctx(ctx).Debug().
|
|
||||||
Stringer("lid", lid).
|
|
||||||
Stringer("pn", portalJID).
|
|
||||||
Msg("Rerouting phone number DM to LID in on-demand history sync")
|
|
||||||
portalJID = lid
|
|
||||||
}
|
|
||||||
}
|
|
||||||
portal, err := wa.Main.Bridge.GetPortalByKey(ctx, wa.makeWAPortalKey(portalJID))
|
|
||||||
if err != nil {
|
|
||||||
zerolog.Ctx(ctx).Err(err).Stringer("portal_jid", portalJID).Msg("Failed to get portal for on-demand history sync")
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
ctx := zerolog.Ctx(ctx).With().
|
|
||||||
Str("portal_id", string(portal.ID)).
|
|
||||||
Str("portal_receiver", string(portal.Receiver)).
|
|
||||||
Stringer("portal_mxid", portal.MXID).
|
|
||||||
Logger().WithContext(ctx)
|
|
||||||
portal.HandleRemoteBackfill(ctx, wa.UserLogin, &simplevent.Backfill{
|
|
||||||
EventMeta: simplevent.EventMeta{
|
|
||||||
Type: bridgev2.RemoteEventBackfill,
|
|
||||||
PortalKey: portal.PortalKey,
|
|
||||||
},
|
|
||||||
GetDataFunc: func(ctx context.Context, portal *bridgev2.Portal) (*bridgev2.FetchMessagesResponse, error) {
|
|
||||||
if len(conv.GetMessages()) == 0 {
|
|
||||||
return &bridgev2.FetchMessagesResponse{}, nil
|
|
||||||
}
|
|
||||||
messages := make([]*waWeb.WebMessageInfo, len(conv.GetMessages()))
|
|
||||||
for i, rawMsg := range conv.GetMessages() {
|
|
||||||
messages[i] = rawMsg.Message
|
|
||||||
}
|
|
||||||
zerolog.Ctx(ctx).Debug().
|
|
||||||
Int("message_count", len(messages)).
|
|
||||||
Stringer("end_of_history_type", conv.GetEndOfHistoryTransferType()).
|
|
||||||
Msg("Converting messages to bridge from on-demand history sync")
|
|
||||||
resp, err := wa.convertHistorySyncMessages(ctx, portal, portalJID, messages, false, false)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
resp.HasMore = conv.GetEndOfHistoryTransferType() == waHistorySync.Conversation_COMPLETE_BUT_MORE_MESSAGES_REMAIN_ON_PRIMARY
|
|
||||||
return resp, nil
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (wa *WhatsAppClient) convertHistorySyncMessage(
|
func (wa *WhatsAppClient) convertHistorySyncMessage(
|
||||||
ctx context.Context, cli *whatsmeow.Client, portal *bridgev2.Portal, info *types.MessageInfo, msg, rawMsg *waE2E.Message, isViewOnce bool, reactions []*waWeb.Reaction,
|
ctx context.Context, portal *bridgev2.Portal, info *types.MessageInfo, msg *waE2E.Message, isViewOnce bool, reactions []*waWeb.Reaction,
|
||||||
) (*bridgev2.BackfillMessage, *wadb.MediaRequest) {
|
) (*bridgev2.BackfillMessage, *wadb.MediaRequest) {
|
||||||
// New messages turn these into edits, but in backfill we only have the last version,
|
|
||||||
// so no need to do the edit thing. Instead, just unwrap the message.
|
|
||||||
if msg.GetAssociatedChildMessage().GetMessage() != nil {
|
|
||||||
msg = msg.GetAssociatedChildMessage().GetMessage()
|
|
||||||
}
|
|
||||||
// TODO use proper intent
|
// TODO use proper intent
|
||||||
intent := wa.Main.Bridge.Bot
|
intent := wa.Main.Bridge.Bot
|
||||||
msgID := waid.MakeMessageIDWithAltSender(info.Chat, info.Sender, info.SenderAlt, info.ID)
|
|
||||||
wrapped := &bridgev2.BackfillMessage{
|
wrapped := &bridgev2.BackfillMessage{
|
||||||
ConvertedMessage: wa.Main.MsgConv.ToMatrix(ctx, portal, cli, intent, msg, rawMsg, info, isViewOnce, true, nil),
|
ConvertedMessage: wa.Main.MsgConv.ToMatrix(ctx, portal, wa.Client, intent, msg, info, isViewOnce, nil),
|
||||||
Sender: wa.makeEventSender(ctx, pickLID(info.Sender, info.SenderAlt)),
|
Sender: wa.makeEventSender(info.Sender),
|
||||||
ID: msgID,
|
ID: waid.MakeMessageID(info.Chat, info.Sender, info.ID),
|
||||||
TxnID: networkid.TransactionID(msgID),
|
TxnID: networkid.TransactionID(waid.MakeMessageID(info.Chat, info.Sender, info.ID)),
|
||||||
Timestamp: info.Timestamp,
|
Timestamp: info.Timestamp,
|
||||||
StreamOrder: info.Timestamp.Unix(),
|
StreamOrder: info.Timestamp.Unix(),
|
||||||
Reactions: make([]*bridgev2.BackfillReaction, 0, len(reactions)),
|
Reactions: make([]*bridgev2.BackfillReaction, len(reactions)),
|
||||||
}
|
}
|
||||||
mediaReq := wa.processFailedMedia(ctx, portal.PortalKey, wrapped.ID, wrapped.ConvertedMessage, true)
|
mediaReq := wa.processFailedMedia(ctx, portal.PortalKey, wrapped.ID, wrapped.ConvertedMessage, true)
|
||||||
for _, reaction := range reactions {
|
for i, reaction := range reactions {
|
||||||
var sender types.JID
|
var sender types.JID
|
||||||
if reaction.GetKey().GetFromMe() {
|
if reaction.GetKey().GetFromMe() {
|
||||||
sender = wa.GetLID()
|
sender = wa.JID
|
||||||
} else if reaction.GetKey().GetParticipant() != "" {
|
} else if reaction.GetKey().GetParticipant() != "" {
|
||||||
sender, _ = types.ParseJID(*reaction.Key.Participant)
|
sender, _ = types.ParseJID(*reaction.Key.Participant)
|
||||||
} else if info.Chat.Server == types.DefaultUserServer || info.Chat.Server == types.HiddenUserServer || info.Chat.Server == types.BotServer {
|
} else if info.Chat.Server == types.DefaultUserServer || info.Chat.Server == types.BotServer {
|
||||||
sender = info.Chat
|
sender = info.Chat
|
||||||
}
|
}
|
||||||
if sender.IsEmpty() {
|
if sender.IsEmpty() {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
wrapped.Reactions = append(wrapped.Reactions, &bridgev2.BackfillReaction{
|
wrapped.Reactions[i] = &bridgev2.BackfillReaction{
|
||||||
TargetPart: ptr.Ptr(networkid.PartID("")),
|
TargetPart: ptr.Ptr(networkid.PartID("")),
|
||||||
Timestamp: time.UnixMilli(reaction.GetSenderTimestampMS()),
|
Timestamp: time.UnixMilli(reaction.GetSenderTimestampMS()),
|
||||||
Sender: wa.makeEventSender(ctx, sender),
|
Sender: wa.makeEventSender(sender),
|
||||||
Emoji: reaction.GetText(),
|
Emoji: reaction.GetText(),
|
||||||
})
|
}
|
||||||
}
|
}
|
||||||
return wrapped, mediaReq
|
return wrapped, mediaReq
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue