mirror of
https://github.com/mautrix/whatsapp.git
synced 2026-05-14 17:56:53 -04:00
Compare commits
1 commit
main
...
fetch-cont
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c29d5632bb |
62 changed files with 1388 additions and 4201 deletions
18
.github/ISSUE_TEMPLATE/bug.md
vendored
18
.github/ISSUE_TEMPLATE/bug.md
vendored
|
|
@ -1,18 +1,14 @@
|
|||
---
|
||||
name: Bug report
|
||||
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
|
||||
is strongly recommended.
|
||||
type: Bug
|
||||
file a bug report. Remember to include relevant logs.
|
||||
labels: bug
|
||||
|
||||
---
|
||||
|
||||
<!-- Include relevant logs, the bridge version and other important details here -->
|
||||
<!--
|
||||
Remember to include relevant logs, the bridge version and any other details.
|
||||
|
||||
### Checklist
|
||||
|
||||
<!-- All items below are mandatory. Issues not following the rules may be closed without comment. -->
|
||||
|
||||
* [ ] This is an actual bug, not just a setup issue (see the [troubleshooting docs](https://docs.mau.fi/bridges/general/troubleshooting.html) or ask in the Matrix room for setup help).
|
||||
* [ ] I am certain that sufficient information is included. Ask in the Matrix room first if not.
|
||||
* [ ] The bug is still present on the main branch. The `!wa version` command output is: ``
|
||||
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.
|
||||
-->
|
||||
|
|
|
|||
2
.github/ISSUE_TEMPLATE/enhancement.md
vendored
2
.github/ISSUE_TEMPLATE/enhancement.md
vendored
|
|
@ -1,6 +1,6 @@
|
|||
---
|
||||
name: Enhancement request
|
||||
about: Submit a feature request or other suggestion
|
||||
type: Feature
|
||||
labels: enhancement
|
||||
|
||||
---
|
||||
|
|
|
|||
8
.github/workflows/go.yml
vendored
8
.github/workflows/go.yml
vendored
|
|
@ -11,14 +11,14 @@ jobs:
|
|||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
go-version: ["1.25", "1.26"]
|
||||
name: Lint ${{ matrix.go-version == '1.26' && '(latest)' || '(old)' }}
|
||||
go-version: ["1.23", "1.24"]
|
||||
name: Lint ${{ matrix.go-version == '1.24' && '(latest)' || '(old)' }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v6
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v6
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: ${{ matrix.go-version }}
|
||||
cache: true
|
||||
|
|
|
|||
2
.github/workflows/stale.yml
vendored
2
.github/workflows/stale.yml
vendored
|
|
@ -17,7 +17,7 @@ jobs:
|
|||
lock-stale:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: dessant/lock-threads@v6
|
||||
- uses: dessant/lock-threads@v5
|
||||
id: lock
|
||||
with:
|
||||
issue-inactive-days: 90
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
repos:
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v6.0.0
|
||||
rev: v5.0.0
|
||||
hooks:
|
||||
- id: trailing-whitespace
|
||||
exclude_types: [markdown]
|
||||
|
|
@ -9,7 +9,7 @@ repos:
|
|||
- id: check-added-large-files
|
||||
|
||||
- repo: https://github.com/tekwizely/pre-commit-golang
|
||||
rev: v1.0.0-rc.4
|
||||
rev: v1.0.0-rc.1
|
||||
hooks:
|
||||
- id: go-imports-repo
|
||||
args:
|
||||
|
|
|
|||
148
CHANGELOG.md
148
CHANGELOG.md
|
|
@ -1,151 +1,3 @@
|
|||
# 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)
|
||||
|
||||
* Fixed edits being bridged multiple times if a single chat had multiple
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM golang:1-alpine3.23 AS builder
|
||||
FROM golang:1-alpine3.21 AS builder
|
||||
|
||||
RUN apk add --no-cache git ca-certificates build-base su-exec olm-dev
|
||||
|
||||
|
|
@ -6,12 +6,12 @@ COPY . /build
|
|||
WORKDIR /build
|
||||
RUN ./build.sh
|
||||
|
||||
FROM alpine:3.23
|
||||
FROM alpine:3.21
|
||||
|
||||
ENV UID=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/docker-run.sh /docker-run.sh
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
ARG DOCKER_HUB="docker.io"
|
||||
|
||||
FROM ${DOCKER_HUB}/alpine:3.23
|
||||
FROM alpine:3.21
|
||||
|
||||
ENV UID=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
|
||||
COPY $EXECUTABLE /usr/bin/mautrix-whatsapp
|
||||
|
|
|
|||
|
|
@ -14,10 +14,10 @@
|
|||
* [x] Typing notifications
|
||||
* [x] Read receipts
|
||||
* [ ] Power level
|
||||
* [x] Membership actions
|
||||
* [x] Invite
|
||||
* [x] Leave
|
||||
* [x] Kick
|
||||
* [ ] Membership actions
|
||||
* [ ] Invite
|
||||
* [ ] Leave
|
||||
* [ ] Kick
|
||||
* [ ] Room metadata changes
|
||||
* [ ] Name
|
||||
* [ ] Avatar
|
||||
|
|
|
|||
4
build.sh
4
build.sh
|
|
@ -1,2 +1,4 @@
|
|||
#!/bin/sh
|
||||
BINARY_NAME=mautrix-whatsapp go tool maubuild "$@"
|
||||
MAUTRIX_VERSION=$(cat go.mod | grep 'maunium.net/go/mautrix ' | awk '{ print $2 }')
|
||||
GO_LDFLAGS="-s -w -X main.Tag=$(git describe --exact-match --tags 2>/dev/null) -X main.Commit=$(git rev-parse HEAD) -X 'main.BuildTime=`date -Iseconds`' -X 'maunium.net/go/mautrix.GoModVersion=$MAUTRIX_VERSION'"
|
||||
go build -ldflags="$GO_LDFLAGS" "$@" ./cmd/mautrix-whatsapp
|
||||
|
|
|
|||
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;
|
||||
|
|
@ -1,23 +1,48 @@
|
|||
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"
|
||||
"go.mau.fi/whatsmeow/appstate"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/matrix"
|
||||
"maunium.net/go/mautrix/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"`
|
||||
|
|
@ -38,12 +63,180 @@ type Error struct {
|
|||
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).GetStore().Contacts.GetAllContacts(r.Context()); err != nil {
|
||||
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",
|
||||
|
|
@ -70,7 +263,7 @@ func legacyProvContacts(w http.ResponseWriter, r *http.Request) {
|
|||
}
|
||||
|
||||
func legacyProvResolveIdentifier(w http.ResponseWriter, r *http.Request) {
|
||||
number := r.PathValue("number")
|
||||
number := mux.Vars(r)["number"]
|
||||
userLogin := m.Matrix.Provisioning.GetLoginForRequest(w, r)
|
||||
if userLogin == nil {
|
||||
return
|
||||
|
|
@ -113,41 +306,3 @@ func legacyProvResolveIdentifier(w http.ResponseWriter, r *http.Request) {
|
|||
},
|
||||
})
|
||||
}
|
||||
|
||||
func provAppStateDebug(w http.ResponseWriter, r *http.Request) {
|
||||
userLogin := m.Matrix.Provisioning.GetLoginForRequest(w, r)
|
||||
if userLogin == nil {
|
||||
return
|
||||
}
|
||||
client := userLogin.Client.(*connector.WhatsAppClient)
|
||||
if client.Client == nil {
|
||||
mautrix.MNotFound.WithMessage("WhatsApp client not connected").Write(w)
|
||||
return
|
||||
}
|
||||
client.Client.AppStateDebugLogs = true
|
||||
err := client.Client.FetchAppState(r.Context(), appstate.WAPatchName(r.PathValue("patch")), r.URL.Query().Get("full") == "1", false)
|
||||
client.Client.AppStateDebugLogs = false
|
||||
if err != nil {
|
||||
mautrix.MUnknown.WithMessage("Failed to fetch app state: %v", err).Write(w)
|
||||
} else {
|
||||
exhttp.WriteEmptyJSONResponse(w, http.StatusOK)
|
||||
}
|
||||
}
|
||||
|
||||
func provRecoverAppStateDebug(w http.ResponseWriter, r *http.Request) {
|
||||
userLogin := m.Matrix.Provisioning.GetLoginForRequest(w, r)
|
||||
if userLogin == nil {
|
||||
return
|
||||
}
|
||||
client := userLogin.Client.(*connector.WhatsAppClient)
|
||||
if client.Client == nil {
|
||||
mautrix.MNotFound.WithMessage("WhatsApp client not connected").Write(w)
|
||||
return
|
||||
}
|
||||
resp, err := client.Client.SendPeerMessage(r.Context(), whatsmeow.BuildAppStateRecoveryRequest(appstate.WAPatchName(r.PathValue("patch"))))
|
||||
if err != nil {
|
||||
mautrix.MUnknown.WithMessage("Failed to send app state recovery request: %v", err).Write(w)
|
||||
} else {
|
||||
exhttp.WriteJSONResponse(w, http.StatusOK, resp)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,13 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"maunium.net/go/mautrix/bridgev2/bridgeconfig"
|
||||
"maunium.net/go/mautrix/bridgev2/matrix/mxmain"
|
||||
|
||||
"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.
|
||||
|
|
@ -14,23 +18,37 @@ var (
|
|||
BuildTime = "unknown"
|
||||
)
|
||||
|
||||
var c = &connector.WhatsAppConnector{}
|
||||
var m = mxmain.BridgeMain{
|
||||
Name: "mautrix-whatsapp",
|
||||
URL: "https://github.com/mautrix/whatsapp",
|
||||
Description: "A Matrix-WhatsApp puppeting bridge.",
|
||||
Version: "26.04",
|
||||
SemCalVer: true,
|
||||
Connector: &connector.WhatsAppConnector{},
|
||||
Version: "0.11.4",
|
||||
Connector: c,
|
||||
}
|
||||
|
||||
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("GET /v1/contacts", legacyProvContacts)
|
||||
m.Matrix.Provisioning.Router.HandleFunc("GET /v1/resolve_identifier/{number}", legacyProvResolveIdentifier)
|
||||
m.Matrix.Provisioning.Router.HandleFunc("POST /v1/pm/{number}", legacyProvResolveIdentifier)
|
||||
m.Matrix.Provisioning.Router.HandleFunc("POST /v1/debug/appstate/{patch}", provAppStateDebug)
|
||||
m.Matrix.Provisioning.Router.HandleFunc("POST /v1/debug/recover-appstate/{patch}", provRecoverAppStateDebug)
|
||||
m.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)
|
||||
|
|
|
|||
|
|
@ -1,24 +0,0 @@
|
|||
//go:build amd64 && cgo && !noplugin
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"plugin"
|
||||
|
||||
"go.mau.fi/util/exerrors"
|
||||
|
||||
"go.mau.fi/mautrix-whatsapp/pkg/connector"
|
||||
)
|
||||
|
||||
func init() {
|
||||
path := os.Getenv("WM_PLUGIN_PATH")
|
||||
if path == "" {
|
||||
return
|
||||
}
|
||||
fmt.Println("Loading plugin from", path)
|
||||
plug := exerrors.Must(plugin.Open(path))
|
||||
sym := exerrors.Must(plug.Lookup("NewClient"))
|
||||
connector.NewMC = sym.(connector.NewMCFunc)
|
||||
}
|
||||
57
go.mod
57
go.mod
|
|
@ -1,53 +1,48 @@
|
|||
module go.mau.fi/mautrix-whatsapp
|
||||
|
||||
go 1.25.0
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.26.2
|
||||
|
||||
tool go.mau.fi/util/cmd/maubuild
|
||||
toolchain go1.24.1
|
||||
|
||||
require (
|
||||
github.com/lib/pq v1.12.3
|
||||
github.com/rs/zerolog v1.35.1
|
||||
github.com/tidwall/gjson v1.18.0
|
||||
go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25
|
||||
github.com/gorilla/mux v1.8.0
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
github.com/lib/pq v1.10.9
|
||||
github.com/rs/zerolog v1.34.0
|
||||
go.mau.fi/util v0.8.6
|
||||
go.mau.fi/webp v0.2.0
|
||||
go.mau.fi/whatsmeow v0.0.0-20260513140310-c551a4055c0f
|
||||
golang.org/x/image v0.39.0
|
||||
golang.org/x/net v0.53.0
|
||||
golang.org/x/sync v0.20.0
|
||||
google.golang.org/protobuf v1.36.11
|
||||
go.mau.fi/whatsmeow v0.0.0-20250411192951-5ab78fadbf91
|
||||
golang.org/x/image v0.26.0
|
||||
golang.org/x/net v0.39.0
|
||||
golang.org/x/sync v0.13.0
|
||||
google.golang.org/protobuf v1.36.6
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
maunium.net/go/mautrix v0.27.1-0.20260513120123-5fba7e3afae4
|
||||
maunium.net/go/mautrix v0.23.3-0.20250405234116-e675a3c09c38
|
||||
)
|
||||
|
||||
require (
|
||||
filippo.io/edwards25519 v1.2.0 // indirect
|
||||
github.com/beeper/argo-go v1.1.2 // indirect
|
||||
github.com/coder/websocket v1.8.14 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.7.0 // indirect
|
||||
github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect
|
||||
filippo.io/edwards25519 v1.1.0 // indirect
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.14 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.44 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.24 // indirect
|
||||
github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203 // indirect
|
||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect
|
||||
github.com/tidwall/match v1.2.0 // indirect
|
||||
github.com/tidwall/gjson v1.18.0 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
github.com/tidwall/pretty v1.2.1 // indirect
|
||||
github.com/tidwall/sjson v1.2.5 // indirect
|
||||
github.com/vektah/gqlparser/v2 v2.5.27 // indirect
|
||||
github.com/yuin/goldmark v1.8.2 // indirect
|
||||
go.mau.fi/libsignal v0.2.1 // indirect
|
||||
go.mau.fi/zeroconfig v0.2.0 // indirect
|
||||
golang.org/x/crypto v0.50.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
|
||||
golang.org/x/mod v0.35.0 // indirect
|
||||
golang.org/x/sys v0.43.0 // indirect
|
||||
golang.org/x/text v0.36.0 // indirect
|
||||
github.com/yuin/goldmark v1.7.8 // indirect
|
||||
go.mau.fi/libsignal v0.1.2 // indirect
|
||||
go.mau.fi/zeroconfig v0.1.3 // indirect
|
||||
golang.org/x/crypto v0.37.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
golang.org/x/text v0.24.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
maunium.net/go/mauflag v1.0.0 // indirect
|
||||
|
|
|
|||
118
go.sum
118
go.sum
|
|
@ -1,26 +1,21 @@
|
|||
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||
filippo.io/edwards25519 v1.1.0 h1:FNf4tywRC1HmFuKW5xopWpigGjJKiJSV0Cqo0cJWDaA=
|
||||
filippo.io/edwards25519 v1.1.0/go.mod h1:BxyFTGdWcka3PhytdK4V28tE5sGfRvvvRV7EaN4VDT4=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2 h1:OcvFkGmslmlZibjAjaHm3L//6LiuBgolP7OputlJIzU=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.2/go.mod h1:88MAG/4G7SMwSE3CeA0ZKzrT5CiOU3OJ+JlNzwDqpNU=
|
||||
github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM=
|
||||
github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU=
|
||||
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.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||
github.com/coreos/go-systemd/v22 v22.7.0 h1:LAEzFkke61DFROc7zNLX/WA2i5J8gYqe0rSj9KI28KA=
|
||||
github.com/coreos/go-systemd/v22 v22.7.0/go.mod h1:xNUYtjHu2EDXbsxz1i41wouACIwT7Ybq9o0BQhMwD0w=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs=
|
||||
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/elliotchance/orderedmap/v3 v3.1.0 h1:j4DJ5ObEmMBt/lcwIecKcoRxIQUEnw0L804lXYDt/pg=
|
||||
github.com/elliotchance/orderedmap/v3 v3.1.0/go.mod h1:G+Hc2RwaZvJMcS4JpGCOyViCnGeKf0bTYCGTO4uhjSo=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/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.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
|
|
@ -28,17 +23,21 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/lib/pq v1.12.3 h1:tTWxr2YLKwIvK90ZXEw8GP7UFHtcbTtty8zsI+YjrfQ=
|
||||
github.com/lib/pq v1.12.3/go.mod h1:/p+8NSbOcwzAEI7wiMXFlgydTwcgTr3OSKMsD2BitpA=
|
||||
github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw=
|
||||
github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE=
|
||||
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
|
||||
github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
|
||||
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81 h1:WDsQxOJDy0N1VRAjXLpi8sCEZRSGarLWQevDxpTBRrM=
|
||||
github.com/petermattis/goid v0.0.0-20260330135022-df67b199bc81/go.mod h1:pxMtw7cyUw6B2bRH0ZBANSPg+AoSud1I1iyJHI69jH4=
|
||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/petermattis/goid v0.0.0-20250303134427-723919f7f203 h1:E7Kmf11E4K7B5hDti2K2NqPb1nlYlGYsu02S1JNd/Bs=
|
||||
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/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
|
|
@ -46,58 +45,53 @@ github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjR
|
|||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/rs/zerolog v1.35.1 h1:m7xQeoiLIiV0BCEY4Hs+j2NG4Gp2o2KPKmhnnLiazKI=
|
||||
github.com/rs/zerolog v1.35.1/go.mod h1:EjML9kdfa/RMA7h/6z6pYmq1ykOuA8/mjWaEvGI+jcw=
|
||||
github.com/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/rs/zerolog v1.34.0 h1:k43nTLIwcTVQAncfCw4KZ2VY6ukYoZaBPNOE8txlOeY=
|
||||
github.com/rs/zerolog v1.34.0/go.mod h1:bJsvje4Z08ROH4Nhs5iH600c3IkWhwp44iRc54W6wYQ=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
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.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/match v1.2.0 h1:0pt8FlkOwjN2fPt4bIl4BoNxb98gGHN2ObFEDkrfZnM=
|
||||
github.com/tidwall/match v1.2.0/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||
github.com/vektah/gqlparser/v2 v2.5.27 h1:RHPD3JOplpk5mP5JGX8RKZkt2/Vwj/PZv0HxTdwFp0s=
|
||||
github.com/vektah/gqlparser/v2 v2.5.27/go.mod h1:D1/VCZtV3LPnQrcPBeR/q5jkSQIPti0uYCP/RI0gIeo=
|
||||
github.com/yuin/goldmark v1.8.2 h1:kEGpgqJXdgbkhcOgBxkC0X0PmoPG1ZyoZ117rDVp4zE=
|
||||
github.com/yuin/goldmark v1.8.2/go.mod h1:ip/1k0VRfGynBgxOz0yCqHrbZXhcjxyuS66Brc7iBKg=
|
||||
go.mau.fi/libsignal v0.2.1 h1:vRZG4EzTn70XY6Oh/pVKrQGuMHBkAWlGRC22/85m9L0=
|
||||
go.mau.fi/libsignal v0.2.1/go.mod h1:iVvjrHyfQqWajOUaMEsIfo3IqgVMrhWcPiiEzk7NgoU=
|
||||
go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25 h1:YPEmc+li7TF6C9AdRTcSLMb6yCHdF27/wNT7kFLIVNg=
|
||||
go.mau.fi/util v0.9.9-0.20260511124621-9241e81bdf25/go.mod h1:jE9FfhbgEgAwxei6lomO9v8zdCIATcquONUu4vjRwSs=
|
||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
go.mau.fi/libsignal v0.1.2 h1:Vs16DXWxSKyzVtI+EEXLCSy5pVWzzCzp/2eqFGvLyP0=
|
||||
go.mau.fi/libsignal v0.1.2/go.mod h1:JpnLSSJptn/s1sv7I56uEMywvz8x4YzxeF5OzdPb6PE=
|
||||
go.mau.fi/util v0.8.6 h1:AEK13rfgtiZJL2YsNK+W4ihhYCuukcRom8WPP/w/L54=
|
||||
go.mau.fi/util v0.8.6/go.mod h1:uNB3UTXFbkpp7xL1M/WvQks90B/L4gvbLpbS0603KOE=
|
||||
go.mau.fi/webp v0.2.0 h1:QVMenHw7JDb4vall5sV75JNBQj9Hw4u8AKbi1QetHvg=
|
||||
go.mau.fi/webp v0.2.0/go.mod h1:VSg9MyODn12Mb5pyG0NIyNFhujrmoFSsZBs8syOZD1Q=
|
||||
go.mau.fi/whatsmeow v0.0.0-20260513140310-c551a4055c0f h1:icWtsD1MH5nlo8mEpHMPZ9+1kgHkjmXQroYi0lHXKZ0=
|
||||
go.mau.fi/whatsmeow v0.0.0-20260513140310-c551a4055c0f/go.mod h1:ijfkzOXauA/Vz/htXEMfOAJSUgglribW5oQeYC9tSSg=
|
||||
go.mau.fi/zeroconfig v0.2.0 h1:e/OGEERqVRRKlgaro7E6bh8xXiKFSXB3eNNIud7FUjU=
|
||||
go.mau.fi/zeroconfig v0.2.0/go.mod h1:J0Vn0prHNOm493oZoQ84kq83ZaNCYZnq+noI1b1eN8w=
|
||||
golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI=
|
||||
golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
|
||||
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
|
||||
golang.org/x/image v0.39.0 h1:skVYidAEVKgn8lZ602XO75asgXBgLj9G/FE3RbuPFww=
|
||||
golang.org/x/image v0.39.0/go.mod h1:sIbmppfU+xFLPIG0FoVUTvyBMmgng1/XAMhQ2ft0hpA=
|
||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||
go.mau.fi/whatsmeow v0.0.0-20250411192951-5ab78fadbf91 h1:eziiUdmaGyhM7Fxn4IMRClBrX1b2sCAFGSBE2Z57yNg=
|
||||
go.mau.fi/whatsmeow v0.0.0-20250411192951-5ab78fadbf91/go.mod h1:aj6MpceCuJ6m712lPEMAh8iAw2TkABx6TnPjMcdG0eE=
|
||||
go.mau.fi/zeroconfig v0.1.3 h1:As9wYDKmktjmNZW5i1vn8zvJlmGKHeVxHVIBMXsm4kM=
|
||||
go.mau.fi/zeroconfig v0.1.3/go.mod h1:NcSJkf180JT+1IId76PcMuLTNa1CzsFFZ0nBygIQM70=
|
||||
golang.org/x/crypto v0.37.0 h1:kJNSjF/Xp7kU0iB2Z+9viTPMW4EqqsrywMXLJOOsXSE=
|
||||
golang.org/x/crypto v0.37.0/go.mod h1:vg+k43peMZ0pUMhYmVAWysMK35e6ioLh3wB8ZCAfbVc=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw=
|
||||
golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM=
|
||||
golang.org/x/image v0.26.0 h1:4XjIFEZWQmCZi6Wv8BoxsDhRU3RVnLX04dToTDAEPlY=
|
||||
golang.org/x/image v0.26.0/go.mod h1:lcxbMFAovzpnJxzXS3nyL83K27tmqtKzIJpctK8YO5c=
|
||||
golang.org/x/net v0.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610=
|
||||
golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI=
|
||||
golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg=
|
||||
golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164=
|
||||
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0=
|
||||
golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU=
|
||||
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 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
|
|
@ -107,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=
|
||||
maunium.net/go/mauflag v1.0.0 h1:YiaRc0tEI3toYtJMRIfjP+jklH45uDHtT80nUamyD4M=
|
||||
maunium.net/go/mauflag v1.0.0/go.mod h1:nLivPOpTpHnpzEh8jEdSL9UqO9+/KBJFmNRlwKfkPeA=
|
||||
maunium.net/go/mautrix v0.27.1-0.20260513120123-5fba7e3afae4 h1:zNC9eVAhw8FhKpM3AxNAh/iy75UEYX91uJUvqqAYlvo=
|
||||
maunium.net/go/mautrix v0.27.1-0.20260513120123-5fba7e3afae4/go.mod h1:3sOGhXi3P1V6/NruTA0gujkvTypXVUraWktCuTGyDuM=
|
||||
maunium.net/go/mautrix v0.23.3-0.20250405234116-e675a3c09c38 h1:fIe2+kYndm3Mm/DwQ4FsODk2DjrLeEeW7tKtZjyERqM=
|
||||
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 {
|
||||
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.
|
||||
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)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import (
|
|||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
|
@ -29,149 +28,35 @@ import (
|
|||
|
||||
var _ bridgev2.BackfillingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
|
||||
func (wa *WhatsAppClient) historySyncLoop(ctx context.Context) {
|
||||
dispatchTimer := time.NewTimer(wa.Main.Config.HistorySync.DispatchWait)
|
||||
const historySyncDispatchWait = 30 * time.Second
|
||||
|
||||
func (wa *WhatsAppClient) historySyncLoop(ctx context.Context) {
|
||||
dispatchTimer := time.NewTimer(historySyncDispatchWait)
|
||||
|
||||
var timerPending atomic.Bool
|
||||
if !wa.isNewLogin && wa.UserLogin.Metadata.(*waid.UserLoginMetadata).HistorySyncPortalsNeedCreating {
|
||||
dispatchTimer.Reset(5 * time.Second)
|
||||
timerPending.Store(true)
|
||||
} else {
|
||||
dispatchTimer.Stop()
|
||||
}
|
||||
if wa.Client.ManualHistorySyncDownload {
|
||||
// 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() {
|
||||
wa.UserLogin.Log.Debug().Msg("Starting history sync loop")
|
||||
for {
|
||||
select {
|
||||
case evt := <-wa.historySyncs:
|
||||
dispatchTimer.Stop()
|
||||
wa.handleWAHistorySync(ctx, evt)
|
||||
dispatchTimer.Reset(historySyncDispatchWait)
|
||||
case <-dispatchTimer.C:
|
||||
timerPending.Store(false)
|
||||
wa.createPortalsFromHistorySync(ctx)
|
||||
case <-ctx.Done():
|
||||
wa.UserLogin.Log.Debug().Msg("Stopping portal creation history sync loop")
|
||||
wa.UserLogin.Log.Debug().Msg("Stopping history sync loop")
|
||||
return
|
||||
}
|
||||
}
|
||||
}()
|
||||
for {
|
||||
var resetTimer 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 = wa.downloadAndSaveWAHistorySyncData(ctx, notif, rowid)
|
||||
// Continue waking up the loop until all queued notifications are processed
|
||||
select {
|
||||
case wa.historySyncWakeup <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
}
|
||||
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) {
|
||||
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 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()
|
||||
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 err != nil {
|
||||
log.Err(err).Msg("Failed to download history sync")
|
||||
return
|
||||
}
|
||||
if blob.GetSyncType() == waHistorySync.HistorySync_ON_DEMAND {
|
||||
wa.handleOnDemandHistorySync(ctx, blob)
|
||||
if err = wa.Main.DB.HSNotif.Delete(ctx, rowid); err != nil {
|
||||
log.Err(err).Msg("Failed to delete queued on-demand history sync notification")
|
||||
} else if err = wa.Client.DeleteMedia(ctx, whatsmeow.MediaHistory, evt.GetDirectPath(), evt.GetFileEncSHA256(), evt.GetEncHandle()); err != nil {
|
||||
log.Err(err).Msg("Failed to delete history sync blob from server")
|
||||
} else {
|
||||
log.Debug().Msg("Finished handling on-demand history sync and deleted history sync blob from server")
|
||||
}
|
||||
return
|
||||
}
|
||||
err = wa.Main.DB.DoTxn(ctx, nil, func(ctx context.Context) (innerErr error) {
|
||||
innerErr = wa.handleWAHistorySync(ctx, evt, blob, true)
|
||||
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")
|
||||
} else {
|
||||
resetTimer = blob.GetSyncType() == waHistorySync.HistorySync_INITIAL_BOOTSTRAP ||
|
||||
blob.GetSyncType() == waHistorySync.HistorySync_RECENT ||
|
||||
blob.GetSyncType() == waHistorySync.HistorySync_FULL
|
||||
err = wa.Client.DeleteMedia(ctx, whatsmeow.MediaHistory, evt.GetDirectPath(), evt.GetFileEncSHA256(), evt.GetEncHandle())
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to delete history sync blob from server")
|
||||
} else {
|
||||
log.Debug().Msg("Deleted history sync blob from server")
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWAHistorySync(
|
||||
ctx context.Context,
|
||||
notif *waE2E.HistorySyncNotification,
|
||||
evt *waHistorySync.HistorySync,
|
||||
stopOnError bool,
|
||||
) error {
|
||||
func (wa *WhatsAppClient) handleWAHistorySync(ctx context.Context, evt *waHistorySync.HistorySync) {
|
||||
if evt == nil || evt.SyncType == nil {
|
||||
return nil
|
||||
return
|
||||
}
|
||||
log := wa.UserLogin.Log.With().
|
||||
Str("action", "store history sync").
|
||||
|
|
@ -183,12 +68,7 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
|||
if evt.GetGlobalSettings() != nil {
|
||||
log.Debug().Interface("global_settings", evt.GetGlobalSettings()).Msg("Got global settings in history sync")
|
||||
}
|
||||
if evt.GetSyncType() == waHistorySync.HistorySync_INITIAL_STATUS_V3 ||
|
||||
evt.GetSyncType() == waHistorySync.HistorySync_PUSH_NAME ||
|
||||
evt.GetSyncType() == waHistorySync.HistorySync_NON_BLOCKING_DATA {
|
||||
if evt.GetSyncType() == waHistorySync.HistorySync_PUSH_NAME {
|
||||
wa.pushNamesSynced.Set()
|
||||
}
|
||||
if evt.GetSyncType() == waHistorySync.HistorySync_INITIAL_STATUS_V3 || evt.GetSyncType() == waHistorySync.HistorySync_PUSH_NAME || evt.GetSyncType() == waHistorySync.HistorySync_NON_BLOCKING_DATA {
|
||||
log.Debug().
|
||||
Int("conversation_count", len(evt.GetConversations())).
|
||||
Int("pushname_count", len(evt.GetPushnames())).
|
||||
|
|
@ -196,57 +76,35 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
|||
Int("recent_sticker_count", len(evt.GetRecentStickers())).
|
||||
Int("past_participant_count", len(evt.GetPastParticipants())).
|
||||
Msg("Ignoring history sync")
|
||||
return nil
|
||||
return
|
||||
}
|
||||
log.Info().
|
||||
Int("conversation_count", len(evt.GetConversations())).
|
||||
Int("past_participant_count", len(evt.GetPastParticipants())).
|
||||
Dict("notification_metadata", zerolog.Dict().
|
||||
Int64("oldest_msg_in_chunk_ts", notif.GetOldestMsgInChunkTimestampSec()).
|
||||
Any("full_request_meta", notif.GetFullHistorySyncOnDemandRequestMetadata()).
|
||||
Any("access_status", notif.GetMessageAccessStatus()).
|
||||
Str("peer_data_request_session_id", notif.GetPeerDataRequestSessionID())).
|
||||
Msg("Storing history sync")
|
||||
start := time.Now()
|
||||
successfullySavedTotal := 0
|
||||
failedToSaveTotal := 0
|
||||
totalMessageCount := 0
|
||||
for _, conv := range evt.GetConversations() {
|
||||
log := log.With().
|
||||
Int("msg_count", len(conv.GetMessages())).
|
||||
Logger()
|
||||
jid, err := types.ParseJID(conv.GetID())
|
||||
if err != nil {
|
||||
totalMessageCount += len(conv.GetMessages())
|
||||
log.Warn().Err(err).
|
||||
Str("chat_jid", conv.GetID()).
|
||||
Int("msg_count", len(conv.GetMessages())).
|
||||
Msg("Failed to parse chat JID in history sync")
|
||||
continue
|
||||
} else if jid.Server == types.BroadcastServer {
|
||||
log.Debug().Stringer("chat_jid", jid).Msg("Skipping broadcast list in history sync")
|
||||
continue
|
||||
} else {
|
||||
}
|
||||
totalMessageCount += len(conv.GetMessages())
|
||||
}
|
||||
if jid.Server == types.HiddenUserServer {
|
||||
pn, err := wa.GetStore().LIDs.GetPNForLID(ctx, jid)
|
||||
if err != nil {
|
||||
log.Err(err).Stringer("lid", jid).Msg("Failed to get PN for LID in history sync")
|
||||
} else if pn.IsEmpty() {
|
||||
log.Warn().Stringer("lid", jid).Msg("No PN found for LID in history sync")
|
||||
} else {
|
||||
log.Debug().
|
||||
Stringer("lid", jid).
|
||||
Stringer("pn", pn).
|
||||
Msg("Rerouting LID DM to phone number in history sync")
|
||||
jid = pn
|
||||
}
|
||||
}
|
||||
log.UpdateContext(func(c zerolog.Context) zerolog.Context {
|
||||
return c.Stringer("chat_jid", jid)
|
||||
})
|
||||
log := log.With().
|
||||
Stringer("chat_jid", jid).
|
||||
Int("msg_count", len(conv.GetMessages())).
|
||||
Logger()
|
||||
|
||||
var minTime, maxTime, firstItemTime, lastItemTime time.Time
|
||||
var minTime, maxTime time.Time
|
||||
var minTimeIndex, maxTimeIndex int
|
||||
|
||||
ignoredTypes := 0
|
||||
|
|
@ -262,10 +120,6 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
|||
Msg("Dropping historical message due to parse error")
|
||||
continue
|
||||
}
|
||||
if firstItemTime.IsZero() {
|
||||
firstItemTime = msgEvt.Info.Timestamp
|
||||
}
|
||||
lastItemTime = msgEvt.Info.Timestamp
|
||||
if minTime.IsZero() || msgEvt.Info.Timestamp.Before(minTime) {
|
||||
minTime = msgEvt.Info.Timestamp
|
||||
minTimeIndex = i
|
||||
|
|
@ -298,58 +152,37 @@ func (wa *WhatsAppClient) handleWAHistorySync(
|
|||
Int("lowest_time_index", minTimeIndex).
|
||||
Time("highest_time", maxTime).
|
||||
Int("highest_time_index", maxTimeIndex).
|
||||
Time("first_item_time", firstItemTime).
|
||||
Time("last_item_time", lastItemTime).
|
||||
Bool("highest_time_mismatch", firstItemTime != maxTime).
|
||||
Dict("metadata", zerolog.Dict().
|
||||
Uint32("ephemeral_expiration", conv.GetEphemeralExpiration()).
|
||||
Int64("ephemeral_setting_timestamp", conv.GetEphemeralSettingTimestamp()).
|
||||
Uint64("last_message_ts", conv.GetLastMsgTimestamp()).
|
||||
Bool("marked_unread", conv.GetMarkedAsUnread()).
|
||||
Bool("archived", conv.GetArchived()).
|
||||
Uint32("pinned", conv.GetPinned()).
|
||||
Uint64("mute_end", conv.GetMuteEndTime()).
|
||||
Uint32("unread_count", conv.GetUnreadCount()).
|
||||
Bool("end_of_history", conv.GetEndOfHistoryTransfer()).
|
||||
Stringer("end_of_history_type", conv.GetEndOfHistoryTransferType()),
|
||||
Uint32("unread_count", conv.GetUnreadCount()),
|
||||
).
|
||||
Msg("Collected messages to save from history sync conversation")
|
||||
|
||||
if len(messages) > 0 {
|
||||
err = wa.Main.DB.Conversation.Put(ctx, wadb.NewConversation(wa.UserLogin.ID, jid, conv, maxTime))
|
||||
err = wa.Main.DB.Conversation.Put(ctx, wadb.NewConversation(wa.UserLogin.ID, jid, conv))
|
||||
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")
|
||||
continue
|
||||
}
|
||||
err = wa.Main.DB.Message.Put(ctx, wa.UserLogin.ID, jid, messages)
|
||||
if err != nil {
|
||||
if stopOnError {
|
||||
return fmt.Errorf("failed to save messages in %s: %w", jid, err)
|
||||
}
|
||||
log.Err(err).Msg("Failed to save messages")
|
||||
failedToSaveTotal += len(messages)
|
||||
} else {
|
||||
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().
|
||||
Int("total_saved_count", successfullySavedTotal).
|
||||
Int("total_failed_count", failedToSaveTotal).
|
||||
Int("total_message_count", totalMessageCount).
|
||||
Dur("duration", time.Since(start)).
|
||||
Msg("Finished storing history sync")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) {
|
||||
|
|
@ -358,17 +191,13 @@ func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) {
|
|||
Logger()
|
||||
ctx = log.WithContext(ctx)
|
||||
limit := wa.Main.Config.HistorySync.MaxInitialConversations
|
||||
loginTS := wa.UserLogin.Metadata.(*waid.UserLoginMetadata).LoggedInAt
|
||||
conversations, err := wa.Main.DB.Conversation.GetRecent(ctx, wa.UserLogin.ID, limit, loginTS)
|
||||
log.Info().Int("limit", limit).Msg("Creating portals from history sync")
|
||||
conversations, err := wa.Main.DB.Conversation.GetRecent(ctx, wa.UserLogin.ID, limit)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to get recent conversations from database")
|
||||
return
|
||||
}
|
||||
log.Info().
|
||||
Int("limit", limit).
|
||||
Int("conversation_count", len(conversations)).
|
||||
Int64("login_timestamp", loginTS.Unix()).
|
||||
Msg("Creating portals from history sync")
|
||||
log.Info().Int("conversation_count", len(conversations)).Msg("Creating portals from history sync")
|
||||
rateLimitErrors := 0
|
||||
var wg sync.WaitGroup
|
||||
wg.Add(len(conversations))
|
||||
|
|
@ -384,19 +213,10 @@ func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) {
|
|||
if conv.ChatJID == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast {
|
||||
wg.Done()
|
||||
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?
|
||||
select {
|
||||
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
|
||||
}
|
||||
wrappedInfo, err := wa.getChatInfo(ctx, conv.ChatJID, conv, true)
|
||||
time.Sleep(time.Duration(rateLimitErrors) * time.Second)
|
||||
wrappedInfo, err := wa.getChatInfo(ctx, conv.ChatJID, conv)
|
||||
if errors.Is(err, whatsmeow.ErrNotInGroup) {
|
||||
log.Debug().Stringer("chat_jid", conv.ChatJID).
|
||||
Msg("Skipping creating room because the user is not a participant")
|
||||
|
|
@ -416,30 +236,21 @@ func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) {
|
|||
log.Err(err).Stringer("chat_jid", conv.ChatJID).
|
||||
Int("error_count", rateLimitErrors).
|
||||
Msg("Ratelimit error getting chat info, retrying after sleep")
|
||||
select {
|
||||
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
|
||||
}
|
||||
time.Sleep(time.Duration(rateLimitErrors) * time.Minute)
|
||||
continue
|
||||
} else if err != nil {
|
||||
log.Err(err).Stringer("chat_jid", conv.ChatJID).Msg("Failed to get chat info")
|
||||
wg.Done()
|
||||
continue
|
||||
}
|
||||
res := wa.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{
|
||||
wa.Main.Bridge.QueueRemoteEvent(wa.UserLogin, &simplevent.ChatResync{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventChatResync,
|
||||
LogContext: func(c zerolog.Context) zerolog.Context {
|
||||
return c.
|
||||
Stringer("chat_jid", conv.ChatJID).
|
||||
Time("latest_message_ts", conv.LastMessageTimestamp)
|
||||
},
|
||||
LogContext: nil,
|
||||
PortalKey: wa.makeWAPortalKey(conv.ChatJID),
|
||||
CreatePortal: true,
|
||||
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 {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to mark conversation as bridged")
|
||||
}
|
||||
|
|
@ -449,10 +260,6 @@ func (wa *WhatsAppClient) createPortalsFromHistorySync(ctx context.Context) {
|
|||
ChatInfo: wrappedInfo,
|
||||
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")
|
||||
go func() {
|
||||
|
|
@ -473,67 +280,38 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
|
|||
}
|
||||
var markRead bool
|
||||
var startTime, endTime *time.Time
|
||||
var conv *wadb.Conversation
|
||||
if params.Forward || wa.Main.Config.HistorySync.BackwardsOnDemand {
|
||||
conv, err = wa.Main.DB.Conversation.Get(ctx, wa.UserLogin.ID, portalJID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get conversation from database: %w", err)
|
||||
}
|
||||
}
|
||||
if params.Forward {
|
||||
if params.AnchorMessage != nil {
|
||||
startTime = ptr.Ptr(params.AnchorMessage.Timestamp)
|
||||
}
|
||||
if conv != nil {
|
||||
conv, err := wa.Main.DB.Conversation.Get(ctx, wa.UserLogin.ID, portalJID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get conversation from database: %w", err)
|
||||
} else if conv != nil {
|
||||
markRead = !ptr.Val(conv.MarkedAsUnread) && ptr.Val(conv.UnreadCount) == 0
|
||||
}
|
||||
} else {
|
||||
if params.AnchorMessage != nil {
|
||||
endTime = ptr.Ptr(params.AnchorMessage.Timestamp)
|
||||
}
|
||||
if params.Cursor != "" {
|
||||
} else if params.Cursor != "" {
|
||||
endTimeUnix, err := strconv.ParseInt(string(params.Cursor), 10, 64)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse cursor: %w", err)
|
||||
}
|
||||
cursorTime := time.Unix(endTimeUnix, 0)
|
||||
if endTime == nil || cursorTime.Before(*endTime) {
|
||||
endTime = &cursorTime
|
||||
}
|
||||
}
|
||||
}
|
||||
var anchorID types.MessageID
|
||||
if params.AnchorMessage != nil {
|
||||
parsedID, _ := waid.ParseMessageID(params.AnchorMessage.ID)
|
||||
if parsedID != nil {
|
||||
anchorID = parsedID.ID
|
||||
}
|
||||
}
|
||||
var hasMore bool
|
||||
if !params.Forward && wa.Main.Config.HistorySync.BackwardsOnDemand {
|
||||
hasMore = conv != nil && ptr.Val(conv.EndOfHistoryTransferType) == waHistorySync.Conversation_COMPLETE_BUT_MORE_MESSAGES_REMAIN_ON_PRIMARY
|
||||
endTime = ptr.Ptr(time.Unix(endTimeUnix, 0))
|
||||
} else if params.AnchorMessage != nil {
|
||||
endTime = ptr.Ptr(params.AnchorMessage.Timestamp)
|
||||
}
|
||||
messages, err := wa.Main.DB.Message.GetBetween(ctx, wa.UserLogin.ID, portalJID, startTime, endTime, params.Count+1)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load messages from database: %w", err)
|
||||
} else if len(messages) == 0 || (len(messages) == 1 && anchorID != "" && messages[0].GetKey().GetID() == anchorID) {
|
||||
wa.deleteHistorySyncMessages(ctx, portalJID, 0, 0)
|
||||
if hasMore && !params.AllowSlowFetch {
|
||||
return &bridgev2.FetchMessagesResponse{
|
||||
MoreRequiresSlowFetch: true,
|
||||
HasMore: true,
|
||||
Forward: params.Forward,
|
||||
}, nil
|
||||
} else if hasMore {
|
||||
return wa.fetchMessagesFromPhone(ctx, params)
|
||||
}
|
||||
} else if len(messages) == 0 {
|
||||
return &bridgev2.FetchMessagesResponse{
|
||||
HasMore: false,
|
||||
Forward: params.Forward,
|
||||
}, nil
|
||||
}
|
||||
if len(messages) > params.Count {
|
||||
hasMore := false
|
||||
oldestTS := messages[len(messages)-1].GetMessageTimestamp()
|
||||
newestTS := messages[0].GetMessageTimestamp()
|
||||
if len(messages) > params.Count {
|
||||
hasMore = true
|
||||
// For safety, cut off messages with the oldest timestamp in the response.
|
||||
// Otherwise, if there are multiple messages with the same timestamp, the next fetch may miss some.
|
||||
|
|
@ -544,78 +322,17 @@ func (wa *WhatsAppClient) FetchMessages(ctx context.Context, params bridgev2.Fet
|
|||
}
|
||||
}
|
||||
}
|
||||
resp, err := wa.convertHistorySyncMessages(ctx, params.Portal, portalJID, messages, true)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to convert messages: %w", err)
|
||||
}
|
||||
resp.HasMore = hasMore
|
||||
resp.Forward = params.Forward
|
||||
resp.MarkRead = markRead
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) deleteHistorySyncMessages(ctx context.Context, portalJID types.JID, newestTS, oldestTS uint64) {
|
||||
var err error
|
||||
var rows int64
|
||||
if (newestTS == 0 && oldestTS == 0) || !wa.Main.Bridge.Config.Backfill.Queue.AnyEnabled() {
|
||||
// If the backfill queue isn't enabled, delete all messages after backfilling a batch.
|
||||
rows, err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, portalJID)
|
||||
} else {
|
||||
// Otherwise just delete the messages that got backfilled
|
||||
rows, err = wa.Main.DB.Message.DeleteBetween(ctx, wa.UserLogin.ID, portalJID, newestTS, oldestTS)
|
||||
}
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).
|
||||
Stringer("portal_jid", portalJID).
|
||||
Uint64("newest_ts", newestTS).
|
||||
Uint64("oldest_ts", oldestTS).
|
||||
Msg("Failed to delete messages from database after backfill")
|
||||
} else {
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Stringer("portal_jid", portalJID).
|
||||
Uint64("newest_ts", newestTS).
|
||||
Uint64("oldest_ts", oldestTS).
|
||||
Int64("rows_affected", rows).
|
||||
Msg("Deleted history sync messages from database")
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) convertHistorySyncMessages(
|
||||
ctx context.Context,
|
||||
portal *bridgev2.Portal,
|
||||
portalJID types.JID,
|
||||
messages []*waWeb.WebMessageInfo,
|
||||
explodeOnError bool,
|
||||
) (*bridgev2.FetchMessagesResponse, error) {
|
||||
oldestTS := messages[len(messages)-1].GetMessageTimestamp()
|
||||
newestTS := messages[0].GetMessageTimestamp()
|
||||
convertedMessages := make([]*bridgev2.BackfillMessage, 0, len(messages))
|
||||
convertedMessages := make([]*bridgev2.BackfillMessage, len(messages))
|
||||
var mediaRequests []*wadb.MediaRequest
|
||||
for i, msg := range messages {
|
||||
evt, err := wa.Client.ParseWebMessage(portalJID, msg)
|
||||
if err != nil {
|
||||
if explodeOnError {
|
||||
// This should never happen because the info is already parsed once before being stored in the database
|
||||
return nil, fmt.Errorf("failed to parse info of message %s: %w", msg.GetKey().GetID(), err)
|
||||
}
|
||||
zerolog.Ctx(ctx).Warn().Err(err).
|
||||
Int("msg_index", i).
|
||||
Str("msg_id", msg.GetKey().GetID()).
|
||||
Uint64("msg_time_seconds", msg.GetMessageTimestamp()).
|
||||
Msg("Dropping historical message due to parse error")
|
||||
continue
|
||||
}
|
||||
if !explodeOnError {
|
||||
msgType := getMessageType(evt.Message)
|
||||
if msgType == "ignore" || strings.HasPrefix(msgType, "unknown_protocol_") {
|
||||
continue
|
||||
}
|
||||
}
|
||||
var mediaReq *wadb.MediaRequest
|
||||
isViewOnce := evt.IsViewOnce || evt.IsViewOnceV2 || evt.IsViewOnceV2Extension
|
||||
converted, mediaReq := wa.convertHistorySyncMessage(
|
||||
ctx, portal, &evt.Info, evt.Message, evt.RawMessage, isViewOnce, msg.Reactions,
|
||||
)
|
||||
convertedMessages = append(convertedMessages, converted)
|
||||
convertedMessages[i], mediaReq = wa.convertHistorySyncMessage(ctx, params.Portal, &evt.Info, evt.Message, isViewOnce, msg.Reactions)
|
||||
if mediaReq != nil {
|
||||
mediaRequests = append(mediaRequests, mediaReq)
|
||||
}
|
||||
|
|
@ -624,10 +341,24 @@ func (wa *WhatsAppClient) convertHistorySyncMessages(
|
|||
return &bridgev2.FetchMessagesResponse{
|
||||
Messages: convertedMessages,
|
||||
Cursor: networkid.PaginationCursor(strconv.FormatUint(oldestTS, 10)),
|
||||
HasMore: hasMore,
|
||||
Forward: endTime == nil,
|
||||
MarkRead: markRead,
|
||||
// TODO set remaining or total count
|
||||
CompleteCallback: func() {
|
||||
// TODO this only deletes after backfilling. If there's no need for backfill after a relogin,
|
||||
// the messages will be stuck in the database
|
||||
wa.deleteHistorySyncMessages(ctx, portalJID, newestTS, oldestTS)
|
||||
var err error
|
||||
if !wa.Main.Bridge.Config.Backfill.Queue.Enabled && !wa.Main.Bridge.Config.Backfill.WillPaginateManually {
|
||||
// If the backfill queue isn't enabled, delete all messages after backfilling a batch.
|
||||
err = wa.Main.DB.Message.DeleteAllInChat(ctx, wa.UserLogin.ID, portalJID)
|
||||
} else {
|
||||
// Otherwise just delete the messages that got backfilled
|
||||
err = wa.Main.DB.Message.DeleteBetween(ctx, wa.UserLogin.ID, portalJID, newestTS, oldestTS)
|
||||
}
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to delete messages from database after backfill")
|
||||
}
|
||||
if len(mediaRequests) > 0 {
|
||||
go func(ctx context.Context) {
|
||||
for _, req := range mediaRequests {
|
||||
|
|
@ -645,115 +376,22 @@ func (wa *WhatsAppClient) convertHistorySyncMessages(
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) fetchMessagesFromPhone(ctx context.Context, params bridgev2.FetchMessagesParams) (*bridgev2.FetchMessagesResponse, error) {
|
||||
if params.AnchorMessage == nil {
|
||||
return nil, fmt.Errorf("anchor message is required to fetch messages from phone")
|
||||
}
|
||||
parsed, err := waid.ParseMessageID(params.AnchorMessage.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse anchor message ID: %w", err)
|
||||
}
|
||||
|
||||
msgID := wa.Client.GenerateMessageID()
|
||||
reqData := wa.Client.BuildHistorySyncRequest(&types.MessageInfo{
|
||||
MessageSource: types.MessageSource{
|
||||
Chat: parsed.Chat,
|
||||
Sender: parsed.Sender,
|
||||
IsFromMe: parsed.Sender.ToNonAD() == wa.JID.ToNonAD() || parsed.Sender.ToNonAD() == wa.Device.GetLID().ToNonAD(),
|
||||
IsGroup: parsed.Chat.Server == types.GroupServer,
|
||||
},
|
||||
ID: parsed.ID,
|
||||
Timestamp: params.AnchorMessage.Timestamp,
|
||||
}, 50)
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Str("request_msg_id", msgID).
|
||||
Any("anchor_msg_parsed", parsed).
|
||||
Any("request_data", reqData).
|
||||
Msg("Sending history sync request")
|
||||
_, err = wa.Client.SendMessage(ctx, wa.JID.ToNonAD(), reqData, whatsmeow.SendRequestExtra{
|
||||
ID: msgID,
|
||||
Peer: true,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to send history sync request: %w", err)
|
||||
}
|
||||
return &bridgev2.FetchMessagesResponse{
|
||||
HasMore: true,
|
||||
Pending: true,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleOnDemandHistorySync(ctx context.Context, blob *waHistorySync.HistorySync) {
|
||||
if len(blob.GetConversations()) > 1 {
|
||||
zerolog.Ctx(ctx).Warn().
|
||||
Int("conversation_count", len(blob.GetConversations())).
|
||||
Msg("Received on-demand history sync with multiple conversations")
|
||||
}
|
||||
for _, conv := range blob.GetConversations() {
|
||||
portalJID, err := types.ParseJID(conv.GetID())
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Str("jid", conv.GetID()).Msg("Failed to parse portal JID")
|
||||
continue
|
||||
}
|
||||
portal, err := wa.Main.Bridge.GetPortalByKey(ctx, wa.makeWAPortalKey(portalJID))
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Stringer("portal_jid", portalJID).Msg("Failed to get portal for on-demand history sync")
|
||||
continue
|
||||
}
|
||||
ctx := zerolog.Ctx(ctx).With().
|
||||
Str("portal_id", string(portal.ID)).
|
||||
Str("portal_receiver", string(portal.Receiver)).
|
||||
Stringer("portal_mxid", portal.MXID).
|
||||
Logger().WithContext(ctx)
|
||||
portal.HandleRemoteBackfill(ctx, wa.UserLogin, &simplevent.Backfill{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventBackfill,
|
||||
PortalKey: portal.PortalKey,
|
||||
},
|
||||
GetDataFunc: func(ctx context.Context, portal *bridgev2.Portal) (*bridgev2.FetchMessagesResponse, error) {
|
||||
if len(conv.GetMessages()) == 0 {
|
||||
return &bridgev2.FetchMessagesResponse{}, nil
|
||||
}
|
||||
messages := make([]*waWeb.WebMessageInfo, len(conv.GetMessages()))
|
||||
for i, rawMsg := range conv.GetMessages() {
|
||||
messages[i] = rawMsg.Message
|
||||
}
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Int("message_count", len(messages)).
|
||||
Stringer("end_of_history_type", conv.GetEndOfHistoryTransferType()).
|
||||
Msg("Converting messages to bridge from on-demand history sync")
|
||||
resp, err := wa.convertHistorySyncMessages(ctx, portal, portalJID, messages, false)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp.HasMore = conv.GetEndOfHistoryTransferType() == waHistorySync.Conversation_COMPLETE_BUT_MORE_MESSAGES_REMAIN_ON_PRIMARY
|
||||
return resp, nil
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) convertHistorySyncMessage(
|
||||
ctx context.Context, portal *bridgev2.Portal, info *types.MessageInfo, msg, rawMsg *waE2E.Message, isViewOnce bool, reactions []*waWeb.Reaction,
|
||||
ctx context.Context, portal *bridgev2.Portal, info *types.MessageInfo, msg *waE2E.Message, isViewOnce bool, reactions []*waWeb.Reaction,
|
||||
) (*bridgev2.BackfillMessage, *wadb.MediaRequest) {
|
||||
// New messages turn these into edits, but in backfill we only have the last version,
|
||||
// so no need to do the edit thing. Instead, just unwrap the message.
|
||||
if msg.GetAssociatedChildMessage().GetMessage() != nil {
|
||||
msg = msg.GetAssociatedChildMessage().GetMessage()
|
||||
}
|
||||
// TODO use proper intent
|
||||
intent := wa.Main.Bridge.Bot
|
||||
wrapped := &bridgev2.BackfillMessage{
|
||||
ConvertedMessage: wa.Main.MsgConv.ToMatrix(ctx, portal, wa.Client, intent, msg, rawMsg, info, isViewOnce, true, nil),
|
||||
Sender: wa.makeEventSender(ctx, info.Sender),
|
||||
ConvertedMessage: wa.Main.MsgConv.ToMatrix(ctx, portal, wa.Client, intent, msg, info, isViewOnce, nil),
|
||||
Sender: wa.makeEventSender(info.Sender),
|
||||
ID: waid.MakeMessageID(info.Chat, info.Sender, info.ID),
|
||||
TxnID: networkid.TransactionID(waid.MakeMessageID(info.Chat, info.Sender, info.ID)),
|
||||
Timestamp: info.Timestamp,
|
||||
StreamOrder: info.Timestamp.Unix(),
|
||||
Reactions: make([]*bridgev2.BackfillReaction, 0, len(reactions)),
|
||||
Reactions: make([]*bridgev2.BackfillReaction, len(reactions)),
|
||||
}
|
||||
mediaReq := wa.processFailedMedia(ctx, portal.PortalKey, wrapped.ID, wrapped.ConvertedMessage, true)
|
||||
for _, reaction := range reactions {
|
||||
for i, reaction := range reactions {
|
||||
var sender types.JID
|
||||
if reaction.GetKey().GetFromMe() {
|
||||
sender = wa.JID
|
||||
|
|
@ -765,12 +403,12 @@ func (wa *WhatsAppClient) convertHistorySyncMessage(
|
|||
if sender.IsEmpty() {
|
||||
continue
|
||||
}
|
||||
wrapped.Reactions = append(wrapped.Reactions, &bridgev2.BackfillReaction{
|
||||
wrapped.Reactions[i] = &bridgev2.BackfillReaction{
|
||||
TargetPart: ptr.Ptr(networkid.PartID("")),
|
||||
Timestamp: time.UnixMilli(reaction.GetSenderTimestampMS()),
|
||||
Sender: wa.makeEventSender(ctx, sender),
|
||||
Sender: wa.makeEventSender(sender),
|
||||
Emoji: reaction.GetText(),
|
||||
})
|
||||
}
|
||||
}
|
||||
return wrapped, mediaReq
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,6 @@ import (
|
|||
"go.mau.fi/util/jsontime"
|
||||
"go.mau.fi/util/ptr"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/database"
|
||||
"maunium.net/go/mautrix/event"
|
||||
|
||||
"go.mau.fi/mautrix-whatsapp/pkg/waid"
|
||||
|
|
@ -17,34 +16,6 @@ import (
|
|||
var WhatsAppGeneralCaps = &bridgev2.NetworkGeneralCapabilities{
|
||||
DisappearingMessages: true,
|
||||
AggressiveUpdateInfo: true,
|
||||
ImplicitReadReceipts: true,
|
||||
Provisioning: bridgev2.ProvisioningCapabilities{
|
||||
ImagePackImport: true,
|
||||
ResolveIdentifier: bridgev2.ResolveIdentifierCapabilities{
|
||||
CreateDM: true,
|
||||
LookupPhone: true,
|
||||
ContactList: true,
|
||||
},
|
||||
GroupCreation: map[string]bridgev2.GroupTypeCapabilities{
|
||||
"group": {
|
||||
TypeDescription: "a group chat",
|
||||
|
||||
Name: bridgev2.GroupFieldCapability{Allowed: true, MaxLength: 100},
|
||||
Disappear: bridgev2.GroupFieldCapability{Allowed: true, DisappearSettings: waDisappearingCap},
|
||||
Participants: bridgev2.GroupFieldCapability{Allowed: true, Required: true, MinLength: 1},
|
||||
Parent: bridgev2.GroupFieldCapability{Allowed: true},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
var waDisappearingCap = &event.DisappearingTimerCapability{
|
||||
Types: []event.DisappearingType{event.DisappearingTypeAfterSend},
|
||||
Timers: []jsontime.Milliseconds{
|
||||
jsontime.MS(24 * time.Hour), // 24 hours
|
||||
jsontime.MS(7 * 24 * time.Hour), // 7 days
|
||||
jsontime.MS(90 * 24 * time.Hour), // 90 days
|
||||
},
|
||||
}
|
||||
|
||||
func (wa *WhatsAppConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilities {
|
||||
|
|
@ -52,7 +23,7 @@ func (wa *WhatsAppConnector) GetCapabilities() *bridgev2.NetworkGeneralCapabilit
|
|||
}
|
||||
|
||||
func (wa *WhatsAppConnector) GetBridgeInfoVersion() (info, caps int) {
|
||||
return 1, 8
|
||||
return 1, 1
|
||||
}
|
||||
|
||||
const WAMaxFileSize = 2000 * 1024 * 1024
|
||||
|
|
@ -67,7 +38,7 @@ func supportedIfFFmpeg() event.CapabilitySupportLevel {
|
|||
}
|
||||
|
||||
func capID() string {
|
||||
base := "fi.mau.whatsapp.capabilities.2026_05_12"
|
||||
base := "fi.mau.whatsapp.capabilities.2025_01_10"
|
||||
if ffmpeg.Supported() {
|
||||
return base + "+ffmpeg"
|
||||
}
|
||||
|
|
@ -95,8 +66,8 @@ var whatsappCaps = &event.RoomFeatures{
|
|||
File: map[event.CapabilityMsgType]*event.FileFeatures{
|
||||
event.MsgImage: {
|
||||
MimeTypes: map[string]event.CapabilitySupportLevel{
|
||||
"image/png": event.CapLevelFullySupported,
|
||||
"image/jpeg": event.CapLevelFullySupported,
|
||||
"image/png": event.CapLevelPartialSupport,
|
||||
"image/webp": event.CapLevelPartialSupport,
|
||||
"image/gif": supportedIfFFmpeg(),
|
||||
},
|
||||
|
|
@ -126,10 +97,10 @@ var whatsappCaps = &event.RoomFeatures{
|
|||
event.CapMsgSticker: {
|
||||
MimeTypes: map[string]event.CapabilitySupportLevel{
|
||||
"image/webp": event.CapLevelFullySupported,
|
||||
// TODO see if sending lottie is possible
|
||||
//"image/lottie+json": event.CapLevelFullySupported,
|
||||
"image/png": event.CapLevelPartialSupport,
|
||||
"image/jpeg": event.CapLevelPartialSupport,
|
||||
// This will only be accepted if it was imported from WhatsApp
|
||||
"video/lottie+json": event.CapLevelPartialSupport,
|
||||
},
|
||||
Caption: event.CapLevelDropped,
|
||||
MaxSize: WAMaxFileSize,
|
||||
|
|
@ -148,7 +119,6 @@ var whatsappCaps = &event.RoomFeatures{
|
|||
"video/mp4": event.CapLevelFullySupported,
|
||||
"video/3gpp": event.CapLevelFullySupported,
|
||||
"video/webm": supportedIfFFmpeg(),
|
||||
"video/quicktime": supportedIfFFmpeg(),
|
||||
},
|
||||
Caption: event.CapLevelFullySupported,
|
||||
MaxCaptionLength: MaxTextLength,
|
||||
|
|
@ -163,22 +133,12 @@ var whatsappCaps = &event.RoomFeatures{
|
|||
MaxSize: WAMaxFileSize,
|
||||
},
|
||||
},
|
||||
State: event.StateFeatureMap{
|
||||
event.StateRoomName.Type: {Level: event.CapLevelFullySupported},
|
||||
event.StateRoomAvatar.Type: {Level: event.CapLevelFullySupported},
|
||||
event.StateTopic.Type: {Level: event.CapLevelFullySupported},
|
||||
event.StateBeeperDisappearingTimer.Type: {Level: event.CapLevelFullySupported},
|
||||
},
|
||||
MemberActions: event.MemberFeatureMap{
|
||||
event.MemberActionInvite: event.CapLevelFullySupported,
|
||||
event.MemberActionKick: event.CapLevelFullySupported,
|
||||
event.MemberActionLeave: event.CapLevelFullySupported,
|
||||
},
|
||||
MaxTextLength: MaxTextLength,
|
||||
LocationMessage: event.CapLevelFullySupported,
|
||||
Poll: event.CapLevelFullySupported,
|
||||
Reply: event.CapLevelFullySupported,
|
||||
Edit: event.CapLevelFullySupported,
|
||||
EditMaxCount: 10,
|
||||
EditMaxAge: ptr.Ptr(jsontime.S(EditMaxAge)),
|
||||
Delete: event.CapLevelFullySupported,
|
||||
DeleteForMe: false,
|
||||
|
|
@ -187,20 +147,11 @@ var whatsappCaps = &event.RoomFeatures{
|
|||
ReactionCount: 1,
|
||||
ReadReceipts: true,
|
||||
TypingNotifications: true,
|
||||
DisappearingTimer: waDisappearingCap,
|
||||
DeleteChat: true,
|
||||
}
|
||||
|
||||
var whatsappDMCaps *event.RoomFeatures
|
||||
var whatsappCAGCaps *event.RoomFeatures
|
||||
|
||||
func init() {
|
||||
whatsappDMCaps = ptr.Clone(whatsappCaps)
|
||||
whatsappDMCaps.ID = capID() + "+dm"
|
||||
whatsappDMCaps.State = event.StateFeatureMap{
|
||||
event.StateBeeperDisappearingTimer.Type: {Level: event.CapLevelFullySupported},
|
||||
}
|
||||
whatsappDMCaps.MemberActions = nil
|
||||
whatsappCAGCaps = ptr.Clone(whatsappCaps)
|
||||
whatsappCAGCaps.ID = capID() + "+cag"
|
||||
whatsappCAGCaps.Reply = event.CapLevelUnsupported
|
||||
|
|
@ -210,8 +161,6 @@ func init() {
|
|||
func (wa *WhatsAppClient) GetCapabilities(ctx context.Context, portal *bridgev2.Portal) *event.RoomFeatures {
|
||||
if portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup {
|
||||
return whatsappCAGCaps
|
||||
} else if portal.RoomType == database.RoomTypeDM {
|
||||
return whatsappDMCaps
|
||||
}
|
||||
return whatsappCaps
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,41 +26,40 @@ func (wa *WhatsAppClient) GetChatInfo(ctx context.Context, portal *bridgev2.Port
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wa.getChatInfo(ctx, portalJID, nil, portal.MXID == "")
|
||||
return wa.getChatInfo(ctx, portalJID, nil)
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) getChatInfo(ctx context.Context, portalJID types.JID, conv *wadb.Conversation, isNew bool) (wrapped *bridgev2.ChatInfo, err error) {
|
||||
func (wa *WhatsAppClient) getChatInfo(ctx context.Context, portalJID types.JID, conv *wadb.Conversation) (wrapped *bridgev2.ChatInfo, err error) {
|
||||
switch portalJID.Server {
|
||||
case types.DefaultUserServer, types.HiddenUserServer, types.BotServer:
|
||||
wrapped = wa.wrapDMInfo(ctx, portalJID)
|
||||
wrapped = wa.wrapDMInfo(portalJID)
|
||||
case types.BroadcastServer:
|
||||
if portalJID == types.StatusBroadcastJID {
|
||||
wrapped = wa.wrapStatusBroadcastInfo(ctx)
|
||||
wrapped = wa.wrapStatusBroadcastInfo()
|
||||
} else {
|
||||
return nil, fmt.Errorf("broadcast list bridging is currently not supported")
|
||||
}
|
||||
case types.GroupServer:
|
||||
info, err := wa.Client.GetGroupInfo(ctx, portalJID)
|
||||
info, err := wa.Client.GetGroupInfo(portalJID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wrapped = wa.wrapGroupInfo(ctx, info)
|
||||
wrapped = wa.wrapGroupInfo(info)
|
||||
wrapped.ExtraUpdates = bridgev2.MergeExtraUpdaters(wrapped.ExtraUpdates, updatePortalLastSyncAt)
|
||||
case types.NewsletterServer:
|
||||
info, err := wa.Client.GetNewsletterInfo(ctx, portalJID)
|
||||
info, err := wa.Client.GetNewsletterInfo(portalJID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wrapped = wa.wrapNewsletterInfo(ctx, info)
|
||||
wrapped = wa.wrapNewsletterInfo(info)
|
||||
default:
|
||||
return nil, fmt.Errorf("unsupported server %s", portalJID.Server)
|
||||
}
|
||||
wa.addExtrasToWrapped(ctx, portalJID, wrapped, conv, isNew)
|
||||
wa.addExtrasToWrapped(ctx, portalJID, wrapped, conv)
|
||||
return wrapped, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) addExtrasToWrapped(ctx context.Context, portalJID types.JID, wrapped *bridgev2.ChatInfo, conv *wadb.Conversation, isNew bool) {
|
||||
if isNew {
|
||||
func (wa *WhatsAppClient) addExtrasToWrapped(ctx context.Context, portalJID types.JID, wrapped *bridgev2.ChatInfo, conv *wadb.Conversation) {
|
||||
if conv == nil {
|
||||
var err error
|
||||
conv, err = wa.Main.DB.Conversation.Get(ctx, wa.UserLogin.ID, portalJID)
|
||||
|
|
@ -71,13 +70,12 @@ func (wa *WhatsAppClient) addExtrasToWrapped(ctx context.Context, portalJID type
|
|||
if conv != nil {
|
||||
wa.applyHistoryInfo(wrapped, conv)
|
||||
}
|
||||
}
|
||||
wa.applyChatSettings(ctx, portalJID, wrapped)
|
||||
}
|
||||
|
||||
func updatePortalLastSyncAt(_ context.Context, portal *bridgev2.Portal) bool {
|
||||
meta := portal.Metadata.(*waid.PortalMetadata)
|
||||
forceSave := ResyncMinInterval < 24*time.Hour || time.Since(meta.LastSync.Time) > 24*time.Hour
|
||||
forceSave := time.Since(meta.LastSync.Time) > 24*time.Hour
|
||||
meta.LastSync = jsontime.UnixNow()
|
||||
return forceSave
|
||||
}
|
||||
|
|
@ -94,7 +92,7 @@ func updateDisappearingTimerSetAt(ts int64) bridgev2.ExtraUpdater[*bridgev2.Port
|
|||
}
|
||||
|
||||
func (wa *WhatsAppClient) applyChatSettings(ctx context.Context, chatID types.JID, info *bridgev2.ChatInfo) {
|
||||
chat, err := wa.GetStore().ChatSettings.GetChatSettings(ctx, chatID)
|
||||
chat, err := wa.GetStore().ChatSettings.GetChatSettings(chatID)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to get chat settings")
|
||||
return
|
||||
|
|
@ -124,7 +122,7 @@ func (wa *WhatsAppClient) applyHistoryInfo(info *bridgev2.ChatInfo, conv *wadb.C
|
|||
}
|
||||
if info.Disappear == nil && ptr.Val(conv.EphemeralExpiration) > 0 {
|
||||
info.Disappear = &database.DisappearingSetting{
|
||||
Type: event.DisappearingTypeAfterSend,
|
||||
Type: database.DisappearingTypeAfterRead,
|
||||
Timer: time.Duration(*conv.EphemeralExpiration) * time.Second,
|
||||
}
|
||||
if conv.EphemeralSettingTimestamp != nil {
|
||||
|
|
@ -140,7 +138,7 @@ const UnnamedBroadcastName = "Unnamed broadcast list"
|
|||
const PrivateChatTopic = "WhatsApp private chat"
|
||||
const BotChatTopic = "WhatsApp chat with a bot"
|
||||
|
||||
func (wa *WhatsAppClient) wrapDMInfo(ctx context.Context, jid types.JID) *bridgev2.ChatInfo {
|
||||
func (wa *WhatsAppClient) wrapDMInfo(jid types.JID) *bridgev2.ChatInfo {
|
||||
info := &bridgev2.ChatInfo{
|
||||
Topic: ptr.Ptr(PrivateChatTopic),
|
||||
Members: &bridgev2.ChatMemberList{
|
||||
|
|
@ -148,17 +146,10 @@ func (wa *WhatsAppClient) wrapDMInfo(ctx context.Context, jid types.JID) *bridge
|
|||
TotalMemberCount: 2,
|
||||
OtherUserID: waid.MakeUserID(jid),
|
||||
MemberMap: map[networkid.UserID]bridgev2.ChatMember{
|
||||
waid.MakeUserID(jid): {EventSender: wa.makeEventSender(ctx, jid)},
|
||||
waid.MakeUserID(wa.JID): {EventSender: wa.makeEventSender(ctx, wa.JID)},
|
||||
},
|
||||
PowerLevels: &bridgev2.PowerLevelOverrides{
|
||||
Events: map[event.Type]int{
|
||||
event.StateRoomName: 0,
|
||||
event.StateRoomAvatar: 0,
|
||||
event.StateTopic: 0,
|
||||
event.StateBeeperDisappearingTimer: 0,
|
||||
},
|
||||
waid.MakeUserID(jid): {EventSender: wa.makeEventSender(jid)},
|
||||
waid.MakeUserID(wa.JID): {EventSender: wa.makeEventSender(wa.JID)},
|
||||
},
|
||||
PowerLevels: nil,
|
||||
},
|
||||
Type: ptr.Ptr(database.RoomTypeDM),
|
||||
}
|
||||
|
|
@ -175,7 +166,7 @@ func (wa *WhatsAppClient) wrapDMInfo(ctx context.Context, jid types.JID) *bridge
|
|||
return info
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) wrapStatusBroadcastInfo(ctx context.Context) *bridgev2.ChatInfo {
|
||||
func (wa *WhatsAppClient) wrapStatusBroadcastInfo() *bridgev2.ChatInfo {
|
||||
userLocal := &bridgev2.UserLocalPortalInfo{}
|
||||
if wa.Main.Config.MuteStatusBroadcast {
|
||||
userLocal.MutedUntil = ptr.Ptr(event.MutedForever)
|
||||
|
|
@ -189,7 +180,7 @@ func (wa *WhatsAppClient) wrapStatusBroadcastInfo(ctx context.Context) *bridgev2
|
|||
Members: &bridgev2.ChatMemberList{
|
||||
IsFull: false,
|
||||
MemberMap: map[networkid.UserID]bridgev2.ChatMember{
|
||||
waid.MakeUserID(wa.JID): {EventSender: wa.makeEventSender(ctx, wa.JID)},
|
||||
waid.MakeUserID(wa.JID): {EventSender: wa.makeEventSender(wa.JID)},
|
||||
},
|
||||
},
|
||||
Type: ptr.Ptr(database.RoomTypeDefault),
|
||||
|
|
@ -227,18 +218,7 @@ func setAddressingMode(mode types.AddressingMode) bridgev2.ExtraUpdater[*bridgev
|
|||
}
|
||||
}
|
||||
|
||||
func setTopicID(id, topic string) bridgev2.ExtraUpdater[*bridgev2.Portal] {
|
||||
return func(_ context.Context, portal *bridgev2.Portal) bool {
|
||||
meta := portal.Metadata.(*waid.PortalMetadata)
|
||||
if meta.TopicID != id && portal.Topic == topic {
|
||||
meta.TopicID = id
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupInfo) *bridgev2.ChatInfo {
|
||||
func (wa *WhatsAppClient) wrapGroupInfo(info *types.GroupInfo) *bridgev2.ChatInfo {
|
||||
sendEventPL := defaultPL
|
||||
if info.IsAnnounce && !info.IsDefaultSubGroup {
|
||||
sendEventPL = adminPL
|
||||
|
|
@ -251,7 +231,6 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
|
|||
wa.makePortalAvatarFetcher("", types.EmptyJID, time.Time{}),
|
||||
setDefaultSubGroupFlag(info.IsDefaultSubGroup && info.IsAnnounce),
|
||||
setAddressingMode(info.AddressingMode),
|
||||
setTopicID(info.TopicID, info.Topic),
|
||||
)
|
||||
wrapped := &bridgev2.ChatInfo{
|
||||
Name: ptr.Ptr(info.Name),
|
||||
|
|
@ -271,22 +250,19 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
|
|||
event.StateTopic: metaChangePL,
|
||||
event.EventReaction: defaultPL,
|
||||
event.EventRedaction: defaultPL,
|
||||
|
||||
event.StateBeeperDisappearingTimer: metaChangePL,
|
||||
// TODO always allow poll responses
|
||||
},
|
||||
},
|
||||
},
|
||||
ExcludeChangesFromTimeline: true,
|
||||
Disappear: &database.DisappearingSetting{
|
||||
Type: event.DisappearingTypeAfterSend,
|
||||
Type: database.DisappearingTypeAfterRead,
|
||||
Timer: time.Duration(info.DisappearingTimer) * time.Second,
|
||||
},
|
||||
ExtraUpdates: extraUpdater,
|
||||
}
|
||||
for _, pcp := range info.Participants {
|
||||
member := bridgev2.ChatMember{
|
||||
EventSender: wa.makeEventSender(ctx, pcp.JID),
|
||||
EventSender: wa.makeEventSender(pcp.JID),
|
||||
Membership: event.MembershipJoin,
|
||||
}
|
||||
if pcp.IsSuperAdmin {
|
||||
|
|
@ -296,20 +272,7 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
|
|||
} else {
|
||||
member.PowerLevel = ptr.Ptr(defaultPL)
|
||||
}
|
||||
member.MemberEventExtra = map[string]any{
|
||||
"com.beeper.exclude_from_timeline": true,
|
||||
}
|
||||
wrapped.Members.MemberMap[waid.MakeUserID(pcp.JID)] = member
|
||||
if pcp.JID.Server == types.HiddenUserServer && !pcp.PhoneNumber.IsEmpty() {
|
||||
wrapped.Members.MemberMap[waid.MakeUserID(pcp.PhoneNumber)] = bridgev2.ChatMember{
|
||||
EventSender: bridgev2.EventSender{Sender: waid.MakeUserID(pcp.PhoneNumber)},
|
||||
Membership: event.MembershipLeave,
|
||||
PrevMembership: event.MembershipJoin,
|
||||
MemberEventExtra: map[string]any{
|
||||
"com.beeper.exclude_from_timeline": true,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !info.LinkedParentJID.IsEmpty() {
|
||||
|
|
@ -323,7 +286,7 @@ func (wa *WhatsAppClient) wrapGroupInfo(ctx context.Context, info *types.GroupIn
|
|||
return wrapped
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) wrapGroupInfoChange(ctx context.Context, evt *events.GroupInfo) *bridgev2.ChatInfoChange {
|
||||
func (wa *WhatsAppClient) wrapGroupInfoChange(evt *events.GroupInfo) *bridgev2.ChatInfoChange {
|
||||
var changes *bridgev2.ChatInfo
|
||||
if evt.Name != nil || evt.Topic != nil || evt.Ephemeral != nil || evt.Unlink != nil || evt.Link != nil {
|
||||
changes = &bridgev2.ChatInfo{}
|
||||
|
|
@ -332,11 +295,10 @@ func (wa *WhatsAppClient) wrapGroupInfoChange(ctx context.Context, evt *events.G
|
|||
}
|
||||
if evt.Topic != nil {
|
||||
changes.Topic = &evt.Topic.Topic
|
||||
changes.ExtraUpdates = bridgev2.MergeExtraUpdaters(changes.ExtraUpdates, setTopicID(evt.Topic.TopicID, evt.Topic.Topic))
|
||||
}
|
||||
if evt.Ephemeral != nil {
|
||||
changes.Disappear = &database.DisappearingSetting{
|
||||
Type: event.DisappearingTypeAfterSend,
|
||||
Type: database.DisappearingTypeAfterRead,
|
||||
Timer: time.Duration(evt.Ephemeral.DisappearingTimer) * time.Second,
|
||||
}
|
||||
if !evt.Ephemeral.IsEphemeral {
|
||||
|
|
@ -358,24 +320,24 @@ func (wa *WhatsAppClient) wrapGroupInfoChange(ctx context.Context, evt *events.G
|
|||
}
|
||||
for _, userID := range evt.Join {
|
||||
memberChanges.MemberMap[waid.MakeUserID(userID)] = bridgev2.ChatMember{
|
||||
EventSender: wa.makeEventSender(ctx, userID),
|
||||
EventSender: wa.makeEventSender(userID),
|
||||
}
|
||||
}
|
||||
for _, userID := range evt.Promote {
|
||||
memberChanges.MemberMap[waid.MakeUserID(userID)] = bridgev2.ChatMember{
|
||||
EventSender: wa.makeEventSender(ctx, userID),
|
||||
EventSender: wa.makeEventSender(userID),
|
||||
PowerLevel: ptr.Ptr(adminPL),
|
||||
}
|
||||
}
|
||||
for _, userID := range evt.Demote {
|
||||
memberChanges.MemberMap[waid.MakeUserID(userID)] = bridgev2.ChatMember{
|
||||
EventSender: wa.makeEventSender(ctx, userID),
|
||||
EventSender: wa.makeEventSender(userID),
|
||||
PowerLevel: ptr.Ptr(defaultPL),
|
||||
}
|
||||
}
|
||||
for _, userID := range evt.Leave {
|
||||
memberChanges.MemberMap[waid.MakeUserID(userID)] = bridgev2.ChatMember{
|
||||
EventSender: wa.makeEventSender(ctx, userID),
|
||||
EventSender: wa.makeEventSender(userID),
|
||||
Membership: event.MembershipLeave,
|
||||
}
|
||||
}
|
||||
|
|
@ -421,7 +383,7 @@ func (wa *WhatsAppClient) makePortalAvatarFetcher(avatarID string, sender types.
|
|||
existingID = ""
|
||||
}
|
||||
var wrappedAvatar *bridgev2.Avatar
|
||||
avatar, err := wa.Client.GetProfilePictureInfo(ctx, jid, &whatsmeow.GetProfilePictureParams{
|
||||
avatar, err := wa.Client.GetProfilePictureInfo(jid, &whatsmeow.GetProfilePictureParams{
|
||||
ExistingID: existingID,
|
||||
IsCommunity: portal.RoomType == database.RoomTypeSpace,
|
||||
})
|
||||
|
|
@ -440,34 +402,25 @@ func (wa *WhatsAppClient) makePortalAvatarFetcher(avatarID string, sender types.
|
|||
return false
|
||||
} else if avatar == nil {
|
||||
return false
|
||||
} else if wa.Main.MsgConv.DirectMedia {
|
||||
wrappedAvatar, err = wa.makeDirectMediaAvatar(ctx, jid, avatar, portal.RoomType == database.RoomTypeSpace)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to prepare direct media avatar")
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
wrappedAvatar = &bridgev2.Avatar{
|
||||
ID: networkid.AvatarID(avatar.ID),
|
||||
Get: func(ctx context.Context) ([]byte, error) {
|
||||
return wa.Client.DownloadMediaWithPath(ctx, avatar.DirectPath, nil, nil, nil, 0, "", "")
|
||||
return wa.Client.DownloadMediaWithPath(avatar.DirectPath, nil, nil, nil, 0, "", "")
|
||||
},
|
||||
}
|
||||
}
|
||||
var evtSender bridgev2.EventSender
|
||||
if !sender.IsEmpty() {
|
||||
evtSender = wa.makeEventSender(ctx, sender)
|
||||
}
|
||||
senderIntent, ok := portal.GetIntentFor(ctx, evtSender, wa.UserLogin, bridgev2.RemoteEventChatInfoChange)
|
||||
if !ok {
|
||||
return false
|
||||
evtSender = wa.makeEventSender(sender)
|
||||
}
|
||||
senderIntent := portal.GetIntentFor(ctx, evtSender, wa.UserLogin, bridgev2.RemoteEventChatInfoChange)
|
||||
//lint:ignore SA1019 TODO invent a cleaner way to fetch avatar metadata before updating?
|
||||
return portal.Internal().UpdateAvatar(ctx, wrappedAvatar, senderIntent, ts, false)
|
||||
return portal.Internal().UpdateAvatar(ctx, wrappedAvatar, senderIntent, ts)
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.NewsletterMetadata) *bridgev2.ChatInfo {
|
||||
func (wa *WhatsAppClient) wrapNewsletterInfo(info *types.NewsletterMetadata) *bridgev2.ChatInfo {
|
||||
ownPowerLevel := defaultPL
|
||||
var mutedUntil *time.Time
|
||||
if info.ViewerMeta != nil {
|
||||
|
|
@ -485,22 +438,21 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne
|
|||
}
|
||||
}
|
||||
avatar := &bridgev2.Avatar{}
|
||||
// TODO direct media for newsletter avatars
|
||||
if info.ThreadMeta.Picture != nil {
|
||||
avatar.ID = networkid.AvatarID(info.ThreadMeta.Picture.ID)
|
||||
avatar.Get = func(ctx context.Context) ([]byte, error) {
|
||||
return wa.Client.DownloadMediaWithPath(ctx, info.ThreadMeta.Picture.DirectPath, nil, nil, nil, 0, "", "")
|
||||
return wa.Client.DownloadMediaWithPath(info.ThreadMeta.Picture.DirectPath, nil, nil, nil, 0, "", "")
|
||||
}
|
||||
} else if info.ThreadMeta.Preview.ID != "" {
|
||||
avatar.ID = networkid.AvatarID(info.ThreadMeta.Preview.ID)
|
||||
avatar.Get = func(ctx context.Context) ([]byte, error) {
|
||||
meta, err := wa.Client.GetNewsletterInfo(ctx, info.ID)
|
||||
meta, err := wa.Client.GetNewsletterInfo(info.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to fetch full res avatar info: %w", err)
|
||||
} else if meta.ThreadMeta.Picture == nil {
|
||||
return nil, fmt.Errorf("full res avatar info is missing")
|
||||
}
|
||||
return wa.Client.DownloadMediaWithPath(ctx, meta.ThreadMeta.Picture.DirectPath, nil, nil, nil, 0, "", "")
|
||||
return wa.Client.DownloadMediaWithPath(meta.ThreadMeta.Picture.DirectPath, nil, nil, nil, 0, "", "")
|
||||
}
|
||||
} else {
|
||||
avatar.ID = "remove"
|
||||
|
|
@ -517,7 +469,7 @@ func (wa *WhatsAppClient) wrapNewsletterInfo(ctx context.Context, info *types.Ne
|
|||
TotalMemberCount: info.ThreadMeta.SubscriberCount,
|
||||
MemberMap: map[networkid.UserID]bridgev2.ChatMember{
|
||||
waid.MakeUserID(wa.JID): {
|
||||
EventSender: wa.makeEventSender(ctx, wa.JID),
|
||||
EventSender: wa.makeEventSender(wa.JID),
|
||||
PowerLevel: &ownPowerLevel,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -25,10 +25,9 @@ import (
|
|||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/exsync"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/appstate"
|
||||
waBinary "go.mau.fi/whatsmeow/binary"
|
||||
"go.mau.fi/whatsmeow/proto/waHistorySync"
|
||||
"go.mau.fi/whatsmeow/proto/waWa6"
|
||||
"go.mau.fi/whatsmeow/store"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
|
|
@ -38,24 +37,19 @@ import (
|
|||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
"maunium.net/go/mautrix/bridgev2/status"
|
||||
"maunium.net/go/mautrix/event"
|
||||
|
||||
"go.mau.fi/mautrix-whatsapp/pkg/waid"
|
||||
)
|
||||
|
||||
func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2.UserLogin) error {
|
||||
func (wa *WhatsAppConnector) LoadUserLogin(_ context.Context, login *bridgev2.UserLogin) error {
|
||||
w := &WhatsAppClient{
|
||||
Main: wa,
|
||||
UserLogin: login,
|
||||
MC: noopMCInstance,
|
||||
|
||||
historySyncWakeup: make(chan struct{}, 1),
|
||||
historySyncs: make(chan *waHistorySync.HistorySync, 64),
|
||||
resyncQueue: make(map[types.JID]resyncQueueItem),
|
||||
directMediaRetries: make(map[networkid.MessageID]*directMediaRetry),
|
||||
mediaRetryLock: semaphore.NewWeighted(wa.Config.HistorySync.MediaRequests.MaxAsyncHandle),
|
||||
pushNamesSynced: exsync.NewEvent(),
|
||||
createDedup: exsync.NewSet[types.MessageID](),
|
||||
appStateFullSyncAttempted: make(map[appstate.WAPatchName]time.Time),
|
||||
}
|
||||
login.Client = w
|
||||
|
||||
|
|
@ -66,7 +60,7 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2.
|
|||
|
||||
var err error
|
||||
w.JID = waid.ParseUserLoginID(login.ID, loginMetadata.WADeviceID)
|
||||
w.Device, err = wa.DeviceStore.GetDevice(ctx, w.JID)
|
||||
w.Device, err = wa.DeviceStore.GetDevice(w.JID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -74,18 +68,14 @@ func (wa *WhatsAppConnector) LoadUserLogin(ctx context.Context, login *bridgev2.
|
|||
if w.Device != nil {
|
||||
log := w.UserLogin.Log.With().Str("component", "whatsmeow").Logger()
|
||||
w.Client = whatsmeow.NewClient(w.Device, waLog.Zerolog(log))
|
||||
w.Client.AddEventHandlerWithSuccessStatus(w.handleWAEvent)
|
||||
w.Client.AddEventHandler(w.handleWAEvent)
|
||||
if bridgev2.PortalEventBuffer == 0 {
|
||||
w.Client.SynchronousAck = true
|
||||
w.Client.EnableDecryptedEventBuffer = bridgev2.PortalEventBuffer == 0
|
||||
w.Client.ManualHistorySyncDownload = true
|
||||
w.Client.SendReportingTokens = true
|
||||
}
|
||||
w.Client.AutomaticMessageRerequestFromPhone = true
|
||||
w.Client.GetMessageForRetry = w.trackNotFoundRetry
|
||||
w.Client.PreRetryCallback = w.trackFoundRetry
|
||||
w.Client.BackgroundEventCtx = w.UserLogin.Log.WithContext(wa.Bridge.BackgroundCtx)
|
||||
w.Client.SetForceActiveDeliveryReceipts(wa.Config.ForceActiveDeliveryReceipts)
|
||||
w.Client.InitialAutoReconnect = wa.Config.InitialAutoReconnect
|
||||
w.Client.UseRetryMessageStore = wa.Config.UseWhatsAppRetryStore
|
||||
} else {
|
||||
w.UserLogin.Log.Warn().Stringer("jid", w.JID).Msg("No device found for user in whatsmeow store")
|
||||
}
|
||||
|
|
@ -104,9 +94,8 @@ type WhatsAppClient struct {
|
|||
Client *whatsmeow.Client
|
||||
Device *store.Device
|
||||
JID types.JID
|
||||
MC mClient
|
||||
|
||||
historySyncWakeup chan struct{}
|
||||
historySyncs chan *waHistorySync.HistorySync
|
||||
stopLoops atomic.Pointer[context.CancelFunc]
|
||||
resyncQueue map[types.JID]resyncQueueItem
|
||||
resyncQueueLock sync.Mutex
|
||||
|
|
@ -114,22 +103,16 @@ type WhatsAppClient struct {
|
|||
directMediaRetries map[networkid.MessageID]*directMediaRetry
|
||||
directMediaLock sync.Mutex
|
||||
mediaRetryLock *semaphore.Weighted
|
||||
offlineSyncWaiter atomic.Pointer[chan error]
|
||||
isNewLogin bool
|
||||
pushNamesSynced *exsync.Event
|
||||
lastPresence types.Presence
|
||||
createDedup *exsync.Set[types.MessageID]
|
||||
offlineSyncWaiter chan error
|
||||
|
||||
appStateRecoveryLock sync.Mutex
|
||||
appStateFullSyncAttempted map[appstate.WAPatchName]time.Time
|
||||
lastPhoneOfflineWarning time.Time
|
||||
isNewLogin bool
|
||||
}
|
||||
|
||||
var (
|
||||
_ bridgev2.NetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.PushableNetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.BackgroundSyncingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.ChatViewingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.StickerImportingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
)
|
||||
|
||||
var pushCfg = &bridgev2.PushConfig{
|
||||
|
|
@ -197,41 +180,23 @@ func (wa *WhatsAppClient) Connect(ctx context.Context) {
|
|||
wa.UserLogin.BridgeState.Send(state)
|
||||
return
|
||||
}
|
||||
wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnecting})
|
||||
wa.Main.firstClientConnectOnce.Do(wa.Main.onFirstClientConnect)
|
||||
if err := wa.Main.updateProxy(ctx, wa.Client, false); err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to update proxy")
|
||||
}
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
wa.initMC()
|
||||
wa.startLoops()
|
||||
wa.Client.BackgroundEventCtx = wa.UserLogin.Log.WithContext(wa.Main.Bridge.BackgroundCtx)
|
||||
zerolog.Ctx(ctx).Debug().Msg("Connecting to WhatsApp")
|
||||
if err := wa.Client.ConnectContext(ctx); err != nil {
|
||||
wa.callStopLoops()
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to connect to WhatsApp")
|
||||
if err := wa.Client.Connect(); err != nil {
|
||||
state := status.BridgeState{
|
||||
StateEvent: status.StateUnknownError,
|
||||
Error: WAConnectionFailed,
|
||||
Info: map[string]any{
|
||||
"go_error": err.Error(),
|
||||
},
|
||||
}
|
||||
wa.UserLogin.BridgeState.Send(state)
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) notifyOfflineSyncWaiter(err error) {
|
||||
if ch := wa.offlineSyncWaiter.Load(); ch != nil {
|
||||
select {
|
||||
case *ch <- err:
|
||||
default:
|
||||
wa.UserLogin.Log.Warn().
|
||||
AnErr("dropped_error", err).
|
||||
Msg("Offline sync waiter channel was full, dropping input")
|
||||
}
|
||||
if wa.offlineSyncWaiter != nil {
|
||||
wa.offlineSyncWaiter <- err
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -252,25 +217,20 @@ func (wa *WhatsAppClient) ConnectBackground(ctx context.Context, params *bridgev
|
|||
if wa.Client == nil {
|
||||
return bridgev2.ErrNotLoggedIn
|
||||
}
|
||||
wa.Client.BackgroundEventCtx = wa.UserLogin.Log.WithContext(wa.Main.Bridge.BackgroundCtx)
|
||||
ch := make(chan error, 1)
|
||||
wa.offlineSyncWaiter.Store(&ch)
|
||||
defer wa.offlineSyncWaiter.Store(nil)
|
||||
wa.Main.backgroundConnectOnce.Do(wa.Main.onFirstBackgroundConnect)
|
||||
wa.offlineSyncWaiter = make(chan error)
|
||||
wa.Main.firstClientConnectOnce.Do(wa.Main.onFirstClientConnect)
|
||||
if err := wa.Main.updateProxy(ctx, wa.Client, false); err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to update proxy")
|
||||
}
|
||||
wa.Client.GetClientPayload = func() *waWa6.ClientPayload {
|
||||
payload := wa.GetStore().GetClientPayload()
|
||||
payload := wa.Client.Store.GetClientPayload()
|
||||
payload.ConnectReason = waWa6.ClientPayload_PUSH.Enum()
|
||||
return payload
|
||||
}
|
||||
defer func() {
|
||||
if cli := wa.Client; cli != nil {
|
||||
cli.GetClientPayload = nil
|
||||
}
|
||||
wa.Client.GetClientPayload = nil
|
||||
}()
|
||||
err := wa.Client.ConnectContext(ctx)
|
||||
err := wa.Client.Connect()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -278,7 +238,7 @@ func (wa *WhatsAppClient) ConnectBackground(ctx context.Context, params *bridgev
|
|||
select {
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
case err = <-ch:
|
||||
case err = <-wa.offlineSyncWaiter:
|
||||
if err == nil {
|
||||
var data wrappedPushNotificationData
|
||||
err = json.Unmarshal(params.RawData, &data)
|
||||
|
|
@ -295,7 +255,7 @@ func (wa *WhatsAppClient) ConnectBackground(ctx context.Context, params *bridgev
|
|||
|
||||
func (wa *WhatsAppClient) sendPNData(ctx context.Context, pn string) error {
|
||||
//lint:ignore SA1019 this is supposed to be dangerous
|
||||
resp, err := wa.Client.DangerousInternals().SendIQ(ctx, whatsmeow.DangerousInfoQuery{
|
||||
resp, err := wa.Client.DangerousInternals().SendIQ(whatsmeow.DangerousInfoQuery{
|
||||
Namespace: "urn:xmpp:whatsapp:push",
|
||||
Type: "get",
|
||||
To: types.ServerJID,
|
||||
|
|
@ -303,6 +263,7 @@ func (wa *WhatsAppClient) sendPNData(ctx context.Context, pn string) error {
|
|||
Tag: "pn",
|
||||
Content: pn,
|
||||
}},
|
||||
Context: ctx,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to send pn: %w", err)
|
||||
|
|
@ -317,7 +278,7 @@ func (wa *WhatsAppClient) sendPNData(ctx context.Context, pn string) error {
|
|||
}
|
||||
zerolog.Ctx(ctx).Debug().Str("cat_data", string(catContentBytes)).Msg("Received cat response from sending pn data")
|
||||
//lint:ignore SA1019 this is supposed to be dangerous
|
||||
err = wa.Client.DangerousInternals().SendNode(ctx, waBinary.Node{
|
||||
err = wa.Client.DangerousInternals().SendNode(waBinary.Node{
|
||||
Tag: "ib",
|
||||
Content: []waBinary.Node{{
|
||||
Tag: "cat",
|
||||
|
|
@ -332,14 +293,14 @@ func (wa *WhatsAppClient) sendPNData(ctx context.Context, pn string) error {
|
|||
}
|
||||
|
||||
func (wa *WhatsAppClient) startLoops() {
|
||||
ctx, cancel := context.WithCancel(wa.Main.Bridge.BackgroundCtx)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
oldStop := wa.stopLoops.Swap(&cancel)
|
||||
if oldStop != nil {
|
||||
(*oldStop)()
|
||||
}
|
||||
ctx = wa.UserLogin.Log.WithContext(ctx)
|
||||
go wa.historySyncLoop(ctx)
|
||||
go wa.ghostResyncLoop(ctx)
|
||||
go wa.disconnectWarningLoop(ctx)
|
||||
if mrc := wa.Main.Config.HistorySync.MediaRequests; mrc.AutoRequestMedia && mrc.RequestMethod == MediaRequestMethodLocalTime {
|
||||
go wa.mediaRequestLoop(ctx)
|
||||
}
|
||||
|
|
@ -355,14 +316,10 @@ func (wa *WhatsAppClient) GetStore() *store.Device {
|
|||
return store.NoopDevice
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) callStopLoops() {
|
||||
func (wa *WhatsAppClient) Disconnect() {
|
||||
if stopHistorySyncLoop := wa.stopLoops.Swap(nil); stopHistorySyncLoop != nil {
|
||||
(*stopHistorySyncLoop)()
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) Disconnect() {
|
||||
wa.callStopLoops()
|
||||
if cli := wa.Client; cli != nil {
|
||||
cli.Disconnect()
|
||||
}
|
||||
|
|
@ -370,7 +327,7 @@ func (wa *WhatsAppClient) Disconnect() {
|
|||
|
||||
func (wa *WhatsAppClient) LogoutRemote(ctx context.Context) {
|
||||
if cli := wa.Client; cli != nil {
|
||||
err := cli.Logout(ctx)
|
||||
err := cli.Logout()
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to log out")
|
||||
}
|
||||
|
|
@ -382,99 +339,3 @@ func (wa *WhatsAppClient) LogoutRemote(ctx context.Context) {
|
|||
func (wa *WhatsAppClient) IsLoggedIn() bool {
|
||||
return wa.Client != nil && wa.Client.IsLoggedIn()
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) syncRemoteProfile(ctx context.Context, ghost *bridgev2.Ghost) {
|
||||
ownID := waid.MakeUserID(wa.GetStore().GetJID())
|
||||
if ghost == nil {
|
||||
var err error
|
||||
ghost, err = wa.Main.Bridge.GetExistingGhostByID(ctx, ownID)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to get own ghost to sync remote profile")
|
||||
return
|
||||
} else if ghost == nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
if ghost.ID != ownID {
|
||||
return
|
||||
}
|
||||
name := wa.GetStore().BusinessName
|
||||
if name == "" {
|
||||
name = wa.GetStore().PushName
|
||||
}
|
||||
if name == "" || wa.UserLogin.RemoteProfile.Name == name && wa.UserLogin.RemoteProfile.Avatar == ghost.AvatarMXC {
|
||||
return
|
||||
}
|
||||
wa.UserLogin.RemoteProfile.Name = name
|
||||
wa.UserLogin.RemoteProfile.Avatar = ghost.AvatarMXC
|
||||
err := wa.UserLogin.Save(ctx)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to save remote profile")
|
||||
}
|
||||
// FIXME this might be racy, should invent a proper way to send last state with info filled
|
||||
if wa.Client.IsConnected() {
|
||||
wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
|
||||
}
|
||||
zerolog.Ctx(ctx).Info().Msg("Remote profile updated")
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixViewingChat(ctx context.Context, msg *bridgev2.MatrixViewingChat) error {
|
||||
var presence types.Presence
|
||||
if msg.Portal != nil {
|
||||
presence = types.PresenceAvailable
|
||||
} else {
|
||||
presence = types.PresenceUnavailable
|
||||
}
|
||||
|
||||
if wa.lastPresence != presence {
|
||||
err := wa.updatePresence(ctx, presence)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to set presence when viewing chat")
|
||||
}
|
||||
}
|
||||
|
||||
if msg.Portal == nil || msg.Portal.Metadata.(*waid.PortalMetadata).LastSync.Add(5*time.Minute).After(time.Now()) {
|
||||
// If we resynced this portal within the last 5 minutes, don't do it again
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reset, but don't save, portal last sync time for immediate sync now
|
||||
msg.Portal.Metadata.(*waid.PortalMetadata).LastSync.Time = time.Time{}
|
||||
// Enqueue for the sync, don't block on it completing
|
||||
wa.EnqueuePortalResync(msg.Portal, true)
|
||||
|
||||
if msg.Portal.OtherUserID != "" {
|
||||
// If this is a DM, also sync the ghost of the other user immediately
|
||||
ghost, err := wa.Main.Bridge.GetExistingGhostByID(ctx, msg.Portal.OtherUserID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get ghost for sync: %w", err)
|
||||
} else if ghost == nil {
|
||||
zerolog.Ctx(ctx).Warn().
|
||||
Str("other_user_id", string(msg.Portal.OtherUserID)).
|
||||
Msg("No ghost found for other user in portal")
|
||||
} else {
|
||||
// Reset, but don't save, portal last sync time for immediate sync now
|
||||
ghost.Metadata.(*waid.GhostMetadata).LastSync.Time = time.Time{}
|
||||
wa.EnqueueGhostResync(ghost)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) updatePresence(ctx context.Context, presence types.Presence) error {
|
||||
err := wa.Client.SendPresence(ctx, presence)
|
||||
if err == nil {
|
||||
wa.lastPresence = presence
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) DownloadImagePack(ctx context.Context, url string) (*bridgev2.ImportedImagePack, error) {
|
||||
return wa.Main.MsgConv.DownloadImagePack(ctx, wa.UserLogin.ID, wa.Client, url)
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) ListImagePacks(ctx context.Context) ([]*event.ImagePackMetadata, error) {
|
||||
// TODO
|
||||
return nil, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,15 +17,12 @@
|
|||
package connector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"html"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/exslices"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/appstate"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
|
|
@ -68,7 +65,7 @@ func fnAccept(ce *commands.Event) {
|
|||
ce.Reply("Login not found")
|
||||
} else if !login.Client.IsLoggedIn() {
|
||||
ce.Reply("Not logged in")
|
||||
} else if err = login.Client.(*WhatsAppClient).Client.JoinGroupWithInvite(ce.Ctx, meta.JID, meta.Inviter, meta.Code, meta.Expiration); err != nil {
|
||||
} else if err = login.Client.(*WhatsAppClient).Client.JoinGroupWithInvite(meta.JID, meta.Inviter, meta.Code, meta.Expiration); err != nil {
|
||||
ce.Log.Err(err).Msg("Failed to accept group invite")
|
||||
ce.Reply("Failed to accept group invite: %v", err)
|
||||
} else {
|
||||
|
|
@ -117,12 +114,15 @@ func fnSync(ce *commands.Event) {
|
|||
})
|
||||
ce.React("✅")
|
||||
case "groups":
|
||||
groups, err := wa.Client.GetJoinedGroups(ce.Ctx)
|
||||
groups, err := wa.Client.GetJoinedGroups()
|
||||
if err != nil {
|
||||
ce.Reply("Failed to get joined groups: %v", err)
|
||||
return
|
||||
}
|
||||
for _, group := range groups {
|
||||
wrapped := wa.wrapGroupInfo(group)
|
||||
wrapped.ExtraUpdates = bridgev2.MergeExtraUpdaters(wrapped.ExtraUpdates, updatePortalLastSyncAt)
|
||||
wa.addExtrasToWrapped(ce.Ctx, group.JID, wrapped, nil)
|
||||
login.QueueRemoteEvent(&simplevent.ChatResync{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventChatResync,
|
||||
|
|
@ -130,34 +130,19 @@ func fnSync(ce *commands.Event) {
|
|||
LogContext: logContext,
|
||||
CreatePortal: true,
|
||||
},
|
||||
GetChatInfoFunc: func(ctx context.Context, portal *bridgev2.Portal) (*bridgev2.ChatInfo, error) {
|
||||
wrapped := wa.wrapGroupInfo(ce.Ctx, group)
|
||||
wrapped.ExtraUpdates = bridgev2.MergeExtraUpdaters(wrapped.ExtraUpdates, updatePortalLastSyncAt)
|
||||
wa.addExtrasToWrapped(ce.Ctx, group.JID, wrapped, nil, portal.MXID == "")
|
||||
return wrapped, nil
|
||||
},
|
||||
ChatInfo: wrapped,
|
||||
})
|
||||
}
|
||||
ce.Reply("Queued syncs for %d groups", len(groups))
|
||||
case "contacts":
|
||||
wa.resyncContacts(false, false)
|
||||
wa.resyncContacts(false)
|
||||
ce.React("✅")
|
||||
case "contacts-with-avatars":
|
||||
wa.resyncContacts(true, false)
|
||||
wa.resyncContacts(true)
|
||||
ce.React("✅")
|
||||
case "appstate":
|
||||
names := appstate.AllPatchNames[:]
|
||||
if len(ce.Args) > 1 {
|
||||
names = exslices.CastFuncFilter(ce.Args[1:], func(name string) (appstate.WAPatchName, bool) {
|
||||
if !slices.Contains(appstate.AllPatchNames[:], appstate.WAPatchName(name)) {
|
||||
ce.Reply("Invalid app state name `%s`", name)
|
||||
return "", false
|
||||
}
|
||||
return appstate.WAPatchName(name), true
|
||||
})
|
||||
}
|
||||
for _, name := range names {
|
||||
err := wa.Client.FetchAppState(ce.Ctx, name, true, false)
|
||||
for _, name := range appstate.AllPatchNames {
|
||||
err := wa.Client.FetchAppState(name, true, false)
|
||||
if errors.Is(err, appstate.ErrKeyNotFound) {
|
||||
ce.Reply("Key not found error syncing app state %s: %v\n\nKey requests are sent automatically, and the sync should happen in the background after your phone responds.", name, err)
|
||||
return
|
||||
|
|
@ -204,7 +189,7 @@ func fnInviteLink(ce *commands.Event) {
|
|||
ce.Reply("Can't get invite link to private chat")
|
||||
} else if portalJID.IsBroadcastList() {
|
||||
ce.Reply("Can't get invite link to broadcast list")
|
||||
} else if link, err := wa.Client.GetGroupInviteLink(ce.Ctx, portalJID, reset); err != nil {
|
||||
} else if link, err := wa.Client.GetGroupInviteLink(portalJID, reset); err != nil {
|
||||
ce.Reply("Failed to get invite link: %v", err)
|
||||
} else {
|
||||
ce.Reply(link)
|
||||
|
|
@ -234,14 +219,14 @@ func fnResolveLink(ce *commands.Event) {
|
|||
}
|
||||
wa := login.Client.(*WhatsAppClient)
|
||||
if strings.HasPrefix(ce.Args[0], whatsmeow.InviteLinkPrefix) {
|
||||
group, err := wa.Client.GetGroupInfoFromLink(ce.Ctx, ce.Args[0])
|
||||
group, err := wa.Client.GetGroupInfoFromLink(ce.Args[0])
|
||||
if err != nil {
|
||||
ce.Reply("Failed to get group info: %v", err)
|
||||
return
|
||||
}
|
||||
ce.Reply("That invite link points at %s (`%s`)", group.Name, group.JID)
|
||||
} else if strings.HasPrefix(ce.Args[0], whatsmeow.BusinessMessageLinkPrefix) || strings.HasPrefix(ce.Args[0], whatsmeow.BusinessMessageLinkDirectPrefix) {
|
||||
target, err := wa.Client.ResolveBusinessMessageLink(ce.Ctx, ce.Args[0])
|
||||
target, err := wa.Client.ResolveBusinessMessageLink(ce.Args[0])
|
||||
if err != nil {
|
||||
ce.Reply("Failed to get business info: %v", err)
|
||||
return
|
||||
|
|
@ -256,7 +241,7 @@ func fnResolveLink(ce *commands.Event) {
|
|||
}
|
||||
ce.Reply("That link points at %s (+%s).%s", target.PushName, target.JID.User, message)
|
||||
} else if strings.HasPrefix(ce.Args[0], whatsmeow.ContactQRLinkPrefix) || strings.HasPrefix(ce.Args[0], whatsmeow.ContactQRLinkDirectPrefix) {
|
||||
target, err := wa.Client.ResolveContactQRLink(ce.Ctx, ce.Args[0])
|
||||
target, err := wa.Client.ResolveContactQRLink(ce.Args[0])
|
||||
if err != nil {
|
||||
ce.Reply("Failed to get contact info: %v", err)
|
||||
return
|
||||
|
|
@ -295,7 +280,7 @@ func fnJoin(ce *commands.Event) {
|
|||
wa := login.Client.(*WhatsAppClient)
|
||||
|
||||
if strings.HasPrefix(ce.Args[0], whatsmeow.InviteLinkPrefix) {
|
||||
jid, err := wa.Client.JoinGroupWithLink(ce.Ctx, ce.Args[0])
|
||||
jid, err := wa.Client.JoinGroupWithLink(ce.Args[0])
|
||||
if err != nil {
|
||||
ce.Reply("Failed to join group: %v", err)
|
||||
return
|
||||
|
|
@ -303,12 +288,12 @@ func fnJoin(ce *commands.Event) {
|
|||
ce.Log.Debug().Stringer("group_jid", jid).Msg("User successfully joined WhatsApp group with link")
|
||||
ce.Reply("Successfully joined group `%s`, the portal should be created momentarily", jid)
|
||||
} else if strings.HasPrefix(ce.Args[0], whatsmeow.NewsletterLinkPrefix) {
|
||||
info, err := wa.Client.GetNewsletterInfoWithInvite(ce.Ctx, ce.Args[0])
|
||||
info, err := wa.Client.GetNewsletterInfoWithInvite(ce.Args[0])
|
||||
if err != nil {
|
||||
ce.Reply("Failed to get channel info: %v", err)
|
||||
return
|
||||
}
|
||||
err = wa.Client.FollowNewsletter(ce.Ctx, info.ID)
|
||||
err = wa.Client.FollowNewsletter(info.ID)
|
||||
if err != nil {
|
||||
ce.Reply("Failed to follow channel: %v", err)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@ import (
|
|||
_ "embed"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
up "go.mau.fi/util/configupgrade"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
|
|
@ -49,15 +48,12 @@ type Config struct {
|
|||
DisableViewOnce bool `yaml:"disable_view_once"`
|
||||
ForceActiveDeliveryReceipts bool `yaml:"force_active_delivery_receipts"`
|
||||
DirectMediaAutoRequest bool `yaml:"direct_media_auto_request"`
|
||||
InitialAutoReconnect bool `yaml:"initial_auto_reconnect"`
|
||||
UseWhatsAppRetryStore bool `yaml:"use_whatsapp_retry_store"`
|
||||
|
||||
AnimatedSticker msgconv.AnimatedStickerConfig `yaml:"animated_sticker"`
|
||||
|
||||
HistorySync struct {
|
||||
MaxInitialConversations int `yaml:"max_initial_conversations"`
|
||||
RequestFullSync bool `yaml:"request_full_sync"`
|
||||
DispatchWait time.Duration `yaml:"dispatch_wait"`
|
||||
FullSyncConfig struct {
|
||||
DaysLimit uint32 `yaml:"days_limit"`
|
||||
SizeLimit uint32 `yaml:"size_mb_limit"`
|
||||
|
|
@ -70,8 +66,6 @@ type Config struct {
|
|||
RequestLocalTime int `yaml:"request_local_time"`
|
||||
MaxAsyncHandle int64 `yaml:"max_async_handle"`
|
||||
} `yaml:"media_requests"`
|
||||
|
||||
BackwardsOnDemand bool `yaml:"backwards_on_demand"`
|
||||
} `yaml:"history_sync"`
|
||||
|
||||
displaynameTemplate *template.Template `yaml:"-"`
|
||||
|
|
@ -118,8 +112,6 @@ func upgradeConfig(helper up.Helper) {
|
|||
helper.Copy(up.Bool, "disable_view_once")
|
||||
helper.Copy(up.Bool, "force_active_delivery_receipts")
|
||||
helper.Copy(up.Bool, "direct_media_auto_request")
|
||||
helper.Copy(up.Bool, "initial_auto_reconnect")
|
||||
helper.Copy(up.Bool, "use_whatsapp_retry_store")
|
||||
|
||||
helper.Copy(up.Str, "animated_sticker", "target")
|
||||
helper.Copy(up.Int, "animated_sticker", "args", "width")
|
||||
|
|
@ -128,7 +120,6 @@ func upgradeConfig(helper up.Helper) {
|
|||
|
||||
helper.Copy(up.Int, "history_sync", "max_initial_conversations")
|
||||
helper.Copy(up.Bool, "history_sync", "request_full_sync")
|
||||
helper.Copy(up.Str|up.Int, "history_sync", "dispatch_wait")
|
||||
helper.Copy(up.Int|up.Null, "history_sync", "full_sync_config", "days_limit")
|
||||
helper.Copy(up.Int|up.Null, "history_sync", "full_sync_config", "size_mb_limit")
|
||||
helper.Copy(up.Int|up.Null, "history_sync", "full_sync_config", "storage_quota_mb")
|
||||
|
|
@ -136,7 +127,6 @@ func upgradeConfig(helper up.Helper) {
|
|||
helper.Copy(up.Str, "history_sync", "media_requests", "request_method")
|
||||
helper.Copy(up.Int, "history_sync", "media_requests", "request_local_time")
|
||||
helper.Copy(up.Int, "history_sync", "media_requests", "max_async_handle")
|
||||
helper.Copy(up.Bool, "history_sync", "backwards_on_demand")
|
||||
}
|
||||
|
||||
type DisplaynameParams struct {
|
||||
|
|
@ -153,11 +143,11 @@ type DisplaynameParams struct {
|
|||
|
||||
func (c *Config) FormatDisplayname(jid types.JID, phone string, contact types.ContactInfo) string {
|
||||
var nameBuf strings.Builder
|
||||
if phone == "" && jid.Server == types.DefaultUserServer {
|
||||
if phone == "" {
|
||||
phone = "+" + jid.User
|
||||
if jid.Server != types.DefaultUserServer {
|
||||
phone = jid.User
|
||||
}
|
||||
if contact.RedactedPhone == "" && phone != "" {
|
||||
contact.RedactedPhone = redactPhone(phone)
|
||||
}
|
||||
err := c.displaynameTemplate.Execute(&nameBuf, &DisplaynameParams{
|
||||
ContactInfo: contact,
|
||||
|
|
@ -176,11 +166,6 @@ func (c *Config) FormatDisplayname(jid types.JID, phone string, contact types.Co
|
|||
return nameBuf.String()
|
||||
}
|
||||
|
||||
func redactPhone(phone string) string {
|
||||
// This doesn't keep 2+ digit country codes properly, but whatever
|
||||
return phone[:2] + strings.Repeat("∙", len(phone)-4) + phone[len(phone)-2:]
|
||||
}
|
||||
|
||||
func (wa *WhatsAppConnector) GetConfig() (string, any, up.Upgrader) {
|
||||
return ExampleConfig, &wa.Config, &up.StructUpgrader{
|
||||
SimpleUpgrader: up.SimpleUpgrader(upgradeConfig),
|
||||
|
|
|
|||
|
|
@ -18,37 +18,24 @@ package connector
|
|||
|
||||
import (
|
||||
"context"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/lib/pq"
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/dbutil"
|
||||
"go.mau.fi/util/random"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/proto/waCompanionReg"
|
||||
"go.mau.fi/whatsmeow/store"
|
||||
"go.mau.fi/whatsmeow/store/sqlstore"
|
||||
whatsmeowUpgrades "go.mau.fi/whatsmeow/store/sqlstore/upgrades"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
waLog "go.mau.fi/whatsmeow/util/log"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/commands"
|
||||
"maunium.net/go/mautrix/bridgev2/database"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
"go.mau.fi/mautrix-whatsapp/pkg/connector/wadb"
|
||||
"go.mau.fi/mautrix-whatsapp/pkg/msgconv"
|
||||
"go.mau.fi/mautrix-whatsapp/pkg/waid"
|
||||
)
|
||||
|
||||
type WhatsAppConnector struct {
|
||||
|
|
@ -59,22 +46,16 @@ type WhatsAppConnector struct {
|
|||
DB *wadb.Database
|
||||
|
||||
firstClientConnectOnce sync.Once
|
||||
backgroundConnectOnce sync.Once
|
||||
|
||||
mediaEditCache MediaEditCache
|
||||
mediaEditCacheLock sync.RWMutex
|
||||
stopMediaEditCacheLoop atomic.Pointer[context.CancelFunc]
|
||||
}
|
||||
|
||||
func init() {
|
||||
sqlstore.PostgresArrayWrapper = pq.Array
|
||||
}
|
||||
|
||||
var (
|
||||
_ bridgev2.NetworkConnector = (*WhatsAppConnector)(nil)
|
||||
_ bridgev2.MaxFileSizeingNetwork = (*WhatsAppConnector)(nil)
|
||||
_ bridgev2.StoppableNetwork = (*WhatsAppConnector)(nil)
|
||||
_ bridgev2.NetworkResettingNetwork = (*WhatsAppConnector)(nil)
|
||||
)
|
||||
|
||||
func (wa *WhatsAppConnector) SetMaxFileSize(maxSize int64) {
|
||||
|
|
@ -128,12 +109,11 @@ func (wa *WhatsAppConnector) Init(bridge *bridgev2.Bridge) {
|
|||
store.DeviceProps.Os = proto.String(wa.Config.OSName)
|
||||
store.DeviceProps.RequireFullSync = proto.Bool(wa.Config.HistorySync.RequestFullSync)
|
||||
if fsc := wa.Config.HistorySync.FullSyncConfig; fsc.DaysLimit > 0 && fsc.SizeLimit > 0 && fsc.StorageQuota > 0 {
|
||||
if store.DeviceProps.HistorySyncConfig == nil {
|
||||
store.DeviceProps.HistorySyncConfig = &waCompanionReg.DeviceProps_HistorySyncConfig{}
|
||||
store.DeviceProps.HistorySyncConfig = &waCompanionReg.DeviceProps_HistorySyncConfig{
|
||||
FullSyncDaysLimit: proto.Uint32(fsc.DaysLimit),
|
||||
FullSyncSizeMbLimit: proto.Uint32(fsc.SizeLimit),
|
||||
StorageQuotaMb: proto.Uint32(fsc.StorageQuota),
|
||||
}
|
||||
store.DeviceProps.HistorySyncConfig.FullSyncDaysLimit = proto.Uint32(fsc.DaysLimit)
|
||||
store.DeviceProps.HistorySyncConfig.FullSyncSizeMbLimit = proto.Uint32(fsc.SizeLimit)
|
||||
store.DeviceProps.HistorySyncConfig.StorageQuotaMb = proto.Uint32(fsc.StorageQuota)
|
||||
}
|
||||
platformID, ok := waCompanionReg.DeviceProps_PlatformType_value[strings.ToUpper(wa.Config.BrowserName)]
|
||||
if ok {
|
||||
|
|
@ -142,7 +122,7 @@ func (wa *WhatsAppConnector) Init(bridge *bridgev2.Bridge) {
|
|||
}
|
||||
|
||||
func (wa *WhatsAppConnector) Start(ctx context.Context) error {
|
||||
err := wa.DeviceStore.Upgrade(ctx)
|
||||
err := wa.DeviceStore.Upgrade()
|
||||
if err != nil {
|
||||
return bridgev2.DBUpgradeError{Err: err, Section: "whatsmeow"}
|
||||
}
|
||||
|
|
@ -157,147 +137,27 @@ func (wa *WhatsAppConnector) Start(ctx context.Context) error {
|
|||
return bridgev2.DBUpgradeError{Err: err, Section: "whatsapp"}
|
||||
}
|
||||
|
||||
if !wa.Bridge.Background && wa.Bridge.DB.KV.Get(ctx, "whatsapp_lid_dms_deleted") == "false" {
|
||||
wa.deleteLIDDMsMigration(ctx)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppConnector) deleteLIDDMsMigration(ctx context.Context) {
|
||||
log := zerolog.Ctx(ctx).With().Str("action", "delete lid dms").Logger()
|
||||
portals, err := wa.Bridge.GetAllPortalsWithMXID(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to get portals for LID DM deletion")
|
||||
return
|
||||
}
|
||||
defer wa.Bridge.DB.KV.Set(ctx, "whatsapp_lid_dms_deleted", "true")
|
||||
if len(portals) == 0 {
|
||||
log.Debug().Msg("No portals found")
|
||||
return
|
||||
}
|
||||
portalsByKey := make(map[networkid.PortalKey]*bridgev2.Portal, len(portals))
|
||||
for _, p := range portals {
|
||||
if p.Receiver == "" || p.RoomType != database.RoomTypeDM {
|
||||
continue
|
||||
}
|
||||
portalsByKey[p.PortalKey] = p
|
||||
}
|
||||
_, err = wa.DB.Exec(ctx, "DELETE FROM whatsapp_history_sync_conversation WHERE chat_jid LIKE '%@lid'")
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to remove LID conversations from history sync")
|
||||
}
|
||||
for key, portal := range portalsByKey {
|
||||
parsedID, err := waid.ParsePortalID(key.ID)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("portal_id", string(key.ID)).Msg("Failed to parse portal ID")
|
||||
continue
|
||||
} else if parsedID.Server != types.HiddenUserServer {
|
||||
continue
|
||||
}
|
||||
var pnStr string
|
||||
err = wa.DB.QueryRow(ctx, "SELECT pn FROM whatsmeow_lid_map WHERE lid=$1", parsedID.User).Scan(&pnStr)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Str("portal_id", string(key.ID)).Msg("Failed to get PN for LID portal")
|
||||
continue
|
||||
}
|
||||
key.ID = waid.MakePortalID(types.JID{User: pnStr, Server: types.DefaultUserServer})
|
||||
_, pnPortalExists := portalsByKey[key]
|
||||
if !pnPortalExists {
|
||||
log.Warn().Str("portal_id", string(key.ID)).Msg("PN portal does not exist, not deleting LID DM")
|
||||
continue
|
||||
}
|
||||
err = portal.Delete(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).
|
||||
Object("portal_key", portal.PortalKey).
|
||||
Stringer("portal_mxid", portal.MXID).
|
||||
Msg("Failed to delete LID DM portal from database")
|
||||
continue
|
||||
}
|
||||
err = wa.Bridge.Bot.DeleteRoom(ctx, portal.MXID, false)
|
||||
if err != nil {
|
||||
log.Err(err).
|
||||
Object("portal_key", portal.PortalKey).
|
||||
Stringer("portal_mxid", portal.MXID).
|
||||
Msg("Failed to delete LID DM portal from Matrix")
|
||||
continue
|
||||
}
|
||||
log.Debug().
|
||||
Object("portal_key", portal.PortalKey).
|
||||
Stringer("portal_mxid", portal.MXID).
|
||||
Msg("Deleted LID DM portal")
|
||||
}
|
||||
log.Info().Msg("Finished deleting LID DM portals")
|
||||
}
|
||||
|
||||
func (wa *WhatsAppConnector) Stop() {
|
||||
if stop := wa.stopMediaEditCacheLoop.Swap(nil); stop != nil {
|
||||
if stop := wa.stopMediaEditCacheLoop.Load(); stop != nil {
|
||||
(*stop)()
|
||||
}
|
||||
}
|
||||
|
||||
const kvWAVersion = "whatsapp_web_version"
|
||||
|
||||
var hardcodedWAVersion = store.GetWAVersion()
|
||||
|
||||
func (wa *WhatsAppConnector) onFirstBackgroundConnect() {
|
||||
verStr := wa.Bridge.DB.KV.Get(wa.Bridge.BackgroundCtx, kvWAVersion)
|
||||
if verStr == "" {
|
||||
wa.Bridge.Log.Warn().Msg("No WhatsApp web version number cached in database")
|
||||
return
|
||||
}
|
||||
ver, err := store.ParseVersion(verStr)
|
||||
if err != nil {
|
||||
wa.Bridge.Log.Err(err).Msg("Failed to parse WhatsApp web version number from database")
|
||||
return
|
||||
}
|
||||
wa.Bridge.Log.Debug().
|
||||
Stringer("hardcoded_version", hardcodedWAVersion).
|
||||
Stringer("cached_version", ver).
|
||||
Msg("Using cached WhatsApp web version number")
|
||||
store.SetWAVersion(ver)
|
||||
}
|
||||
|
||||
func (wa *WhatsAppConnector) onFirstClientConnect() {
|
||||
wa.Bridge.Log.Debug().Msg("Fetching latest WhatsApp web version number")
|
||||
ctx := wa.Bridge.BackgroundCtx
|
||||
ver, err := whatsmeow.GetLatestVersion(ctx, &http.Client{
|
||||
Transport: &http.Transport{
|
||||
DialContext: (&net.Dialer{Timeout: 5 * time.Second}).DialContext,
|
||||
TLSHandshakeTimeout: 5 * time.Second,
|
||||
ResponseHeaderTimeout: 5 * time.Second,
|
||||
ForceAttemptHTTP2: true,
|
||||
},
|
||||
Timeout: 10 * time.Second,
|
||||
})
|
||||
ver, err := whatsmeow.GetLatestVersion(nil)
|
||||
if err != nil {
|
||||
wa.Bridge.Log.Err(err).Msg("Failed to get latest WhatsApp web version number")
|
||||
} else {
|
||||
wa.Bridge.Log.Debug().
|
||||
Stringer("hardcoded_version", hardcodedWAVersion).
|
||||
Stringer("hardcoded_version", store.GetWAVersion()).
|
||||
Stringer("latest_version", *ver).
|
||||
Msg("Got latest WhatsApp web version number")
|
||||
store.SetWAVersion(*ver)
|
||||
wa.Bridge.DB.KV.Set(ctx, kvWAVersion, ver.String())
|
||||
}
|
||||
meclCtx, cancel := context.WithCancel(ctx)
|
||||
meclCtx, cancel := context.WithCancel(context.Background())
|
||||
wa.stopMediaEditCacheLoop.Store(&cancel)
|
||||
go wa.mediaEditCacheExpireLoop(meclCtx)
|
||||
}
|
||||
|
||||
func (wa *WhatsAppConnector) GenerateTransactionID(_ id.UserID, _ id.RoomID, _ event.Type) networkid.RawTransactionID {
|
||||
// The "proper" way would be a hash of the user ID among other things, but the hash includes random bytes too,
|
||||
// so nobody can tell the difference if we just generate random bytes.
|
||||
return networkid.RawTransactionID(whatsmeow.WebMessageIDPrefix + strings.ToUpper(hex.EncodeToString(random.Bytes(9))))
|
||||
}
|
||||
|
||||
func (wa *WhatsAppConnector) ResetHTTPTransport() {
|
||||
// No-op for now, whatsmeow doesn't use the shared transport config yet
|
||||
}
|
||||
|
||||
func (wa *WhatsAppConnector) ResetNetworkConnections() {
|
||||
for _, login := range wa.Bridge.GetAllCachedUserLogins() {
|
||||
login.Client.(*WhatsAppClient).Client.ResetConnection()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ import (
|
|||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"sync"
|
||||
|
|
@ -29,7 +28,6 @@ import (
|
|||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/exsync"
|
||||
"go.mau.fi/util/ptr"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/proto/waMmsRetry"
|
||||
"go.mau.fi/whatsmeow/types/events"
|
||||
|
|
@ -39,7 +37,6 @@ import (
|
|||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
"maunium.net/go/mautrix/mediaproxy"
|
||||
|
||||
"go.mau.fi/mautrix-whatsapp/pkg/connector/wadb"
|
||||
"go.mau.fi/mautrix-whatsapp/pkg/msgconv"
|
||||
"go.mau.fi/mautrix-whatsapp/pkg/waid"
|
||||
)
|
||||
|
|
@ -57,106 +54,13 @@ var ErrReloadNeeded = mautrix.RespError{
|
|||
}
|
||||
|
||||
func (wa *WhatsAppConnector) Download(ctx context.Context, mediaID networkid.MediaID, params map[string]string) (mediaproxy.GetMediaResponse, error) {
|
||||
parsedID, err := waid.ParseMediaID(mediaID)
|
||||
parsedID, receiverID, err := waid.ParseMediaID(mediaID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
log := zerolog.Ctx(ctx).With().Any("parsed_media_id", parsedID).Logger()
|
||||
log := zerolog.Ctx(ctx).With().Any("message_id", parsedID).Logger()
|
||||
ctx = log.WithContext(ctx)
|
||||
if parsedID.Message != nil {
|
||||
return wa.downloadMessageDirectMedia(ctx, parsedID, params)
|
||||
} else if parsedID.Avatar != nil {
|
||||
return wa.downloadAvatarDirectMedia(ctx, parsedID, params)
|
||||
} else if parsedID.Sticker != nil {
|
||||
return wa.downloadStickerDirectMedia(ctx, parsedID, params)
|
||||
} else {
|
||||
return nil, fmt.Errorf("unexpected media ID parsing result")
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppConnector) downloadAvatarDirectMedia(ctx context.Context, parsedID *waid.ParsedMediaID, params map[string]string) (mediaproxy.GetMediaResponse, error) {
|
||||
ul := wa.Bridge.GetCachedUserLoginByID(parsedID.UserLogin)
|
||||
if ul == nil {
|
||||
return nil, fmt.Errorf("%w: user login %s not found", bridgev2.ErrNotLoggedIn, parsedID.UserLogin)
|
||||
}
|
||||
waClient := ul.Client.(*WhatsAppClient)
|
||||
if waClient.Client == nil {
|
||||
return nil, fmt.Errorf("no WhatsApp client found on login %s", parsedID.UserLogin)
|
||||
}
|
||||
cachedInfo, err := wa.DB.AvatarCache.Get(ctx, parsedID.Avatar.TargetJID, parsedID.Avatar.AvatarID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get avatar cache entry: %w", err)
|
||||
}
|
||||
if cachedInfo != nil && cachedInfo.Gone {
|
||||
return nil, mautrix.MNotFound.WithMessage("Avatar is no longer available (cached response)")
|
||||
} else if cachedInfo == nil || cachedInfo.Expiry.Time.Before(time.Now().Add(5*time.Minute)) {
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Str("avatar_id", parsedID.Avatar.AvatarID).
|
||||
Msg("Refreshing avatar URL from WhatsApp servers")
|
||||
avatar, err := waClient.Client.GetProfilePictureInfo(ctx, parsedID.Avatar.TargetJID, &whatsmeow.GetProfilePictureParams{
|
||||
IsCommunity: parsedID.Avatar.Community,
|
||||
})
|
||||
if errors.Is(err, whatsmeow.ErrProfilePictureNotSet) ||
|
||||
errors.Is(err, whatsmeow.ErrProfilePictureUnauthorized) ||
|
||||
(err == nil && (avatar == nil || avatar.ID != parsedID.Avatar.AvatarID)) {
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Err(err).
|
||||
Stringer("target_jid", parsedID.Avatar.TargetJID).
|
||||
Bool("is_community", parsedID.Avatar.Community).
|
||||
Str("wanted_avatar_id", parsedID.Avatar.AvatarID).
|
||||
Str("got_avatar_id", ptr.Val(avatar).ID).
|
||||
Msg("Avatar is no longer available")
|
||||
err = wa.DB.AvatarCache.Put(ctx, &wadb.AvatarCacheEntry{
|
||||
EntityJID: parsedID.Avatar.TargetJID,
|
||||
AvatarID: parsedID.Avatar.AvatarID,
|
||||
Gone: true,
|
||||
})
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).
|
||||
Str("avatar_id", parsedID.Avatar.AvatarID).
|
||||
Msg("Failed to mark avatar as gone in cache")
|
||||
}
|
||||
return nil, mautrix.MNotFound.WithMessage("Avatar is no longer available")
|
||||
} else if err != nil {
|
||||
return nil, mautrix.MUnknown.WithMessage("failed to refresh avatar url: %w", err).WithCanRetry(true)
|
||||
}
|
||||
cachedInfo = avatarInfoToCacheEntry(ctx, parsedID.Avatar.TargetJID, avatar)
|
||||
err = wa.DB.AvatarCache.Put(ctx, cachedInfo)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).
|
||||
Str("avatar_id", avatar.ID).
|
||||
Msg("Failed to update avatar cache entry")
|
||||
}
|
||||
}
|
||||
return &mediaproxy.GetMediaResponseFile{
|
||||
Callback: func(w *os.File) (*mediaproxy.FileMeta, error) {
|
||||
return &mediaproxy.FileMeta{}, waClient.Client.DownloadMediaWithPathToFile(
|
||||
ctx, cachedInfo.DirectPath, nil, nil, nil, 0, "", "", w,
|
||||
)
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppConnector) downloadStickerDirectMedia(ctx context.Context, parsedID *waid.ParsedMediaID, params map[string]string) (mediaproxy.GetMediaResponse, error) {
|
||||
ul := wa.Bridge.GetCachedUserLoginByID(parsedID.UserLogin)
|
||||
if ul == nil {
|
||||
return nil, fmt.Errorf("%w: user login %s not found", bridgev2.ErrNotLoggedIn, parsedID.UserLogin)
|
||||
}
|
||||
waClient := ul.Client.(*WhatsAppClient)
|
||||
if waClient.Client == nil {
|
||||
return nil, fmt.Errorf("no WhatsApp client found on login %s", parsedID.UserLogin)
|
||||
}
|
||||
sticker, err := wa.MsgConv.GetCachedSticker(ctx, waClient.Client, parsedID.Sticker.PackID, parsedID.Sticker.FileHash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if sticker == nil {
|
||||
return nil, mautrix.MNotFound.WithMessage("Sticker not found in pack")
|
||||
}
|
||||
return wa.makeDirectMediaResponse(ctx, waClient, sticker, sticker.MimeType, "", nil, params)
|
||||
}
|
||||
|
||||
func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, parsedID *waid.ParsedMediaID, params map[string]string) (mediaproxy.GetMediaResponse, error) {
|
||||
msg, err := wa.Bridge.DB.Message.GetFirstPartByID(ctx, parsedID.UserLogin, parsedID.Message.String())
|
||||
msg, err := wa.Bridge.DB.Message.GetFirstPartByID(ctx, receiverID, parsedID.String())
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get message: %w", err)
|
||||
} else if msg == nil {
|
||||
|
|
@ -172,8 +76,8 @@ func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, par
|
|||
return nil, fmt.Errorf("failed to unmarshal media keys: %w", err)
|
||||
}
|
||||
var ul *bridgev2.UserLogin
|
||||
if parsedID.UserLogin != "" {
|
||||
ul = wa.Bridge.GetCachedUserLoginByID(parsedID.UserLogin)
|
||||
if receiverID != "" {
|
||||
ul = wa.Bridge.GetCachedUserLoginByID(receiverID)
|
||||
} else {
|
||||
logins, err := wa.Bridge.GetUserLoginsInPortal(ctx, msg.Room)
|
||||
if err != nil {
|
||||
|
|
@ -187,67 +91,38 @@ func (wa *WhatsAppConnector) downloadMessageDirectMedia(ctx context.Context, par
|
|||
}
|
||||
}
|
||||
if ul == nil || !ul.Client.IsLoggedIn() {
|
||||
return nil, bridgev2.ErrNotLoggedIn
|
||||
return nil, fmt.Errorf("no logged in user found")
|
||||
}
|
||||
waClient := ul.Client.(*WhatsAppClient)
|
||||
if waClient.Client == nil {
|
||||
return nil, fmt.Errorf("no WhatsApp client found on login")
|
||||
}
|
||||
return wa.makeDirectMediaResponse(ctx, waClient, keys, keys.MimeType, msg.ID, keys, params)
|
||||
}
|
||||
|
||||
func (wa *WhatsAppConnector) makeDirectMediaResponse(
|
||||
ctx context.Context,
|
||||
waClient *WhatsAppClient,
|
||||
dm whatsmeow.DownloadableMessage,
|
||||
mimeType string,
|
||||
msgID networkid.MessageID,
|
||||
keys *msgconv.FailedMediaKeys,
|
||||
params map[string]string,
|
||||
) (mediaproxy.GetMediaResponse, error) {
|
||||
return &mediaproxy.GetMediaResponseFile{
|
||||
Callback: func(f *os.File) (*mediaproxy.FileMeta, error) {
|
||||
log := zerolog.Ctx(ctx)
|
||||
err := waClient.Client.DownloadToFile(ctx, dm, f)
|
||||
if keys != nil && (errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith403) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith410) || errors.Is(err, whatsmeow.ErrNoURLPresent)) {
|
||||
Callback: func(f *os.File) error {
|
||||
err := waClient.Client.DownloadToFile(keys, f)
|
||||
if errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith403) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) || errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith410) {
|
||||
val := params["fi.mau.whatsapp.reload_media"]
|
||||
if val == "false" || (!wa.Config.DirectMediaAutoRequest && val != "true") {
|
||||
return nil, ErrReloadNeeded
|
||||
return ErrReloadNeeded
|
||||
}
|
||||
log.Trace().Msg("Media not found for direct download, requesting and waiting")
|
||||
err = waClient.requestAndWaitDirectMedia(ctx, msgID, keys)
|
||||
err = waClient.requestAndWaitDirectMedia(ctx, msg.ID, keys)
|
||||
if err != nil {
|
||||
log.Trace().Err(err).Msg("Failed to wait for media for direct download")
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
log.Trace().Msg("Retrying download after successful retry")
|
||||
err = waClient.Client.DownloadToFile(ctx, keys, f)
|
||||
err = waClient.Client.DownloadToFile(keys, f)
|
||||
}
|
||||
if errors.Is(err, whatsmeow.ErrFileLengthMismatch) || errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Mismatching media checksums in message. Ignoring because WhatsApp seems to ignore them too")
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
if mimeType == "application/was" {
|
||||
if _, err := f.Seek(0, io.SeekStart); err != nil {
|
||||
return nil, fmt.Errorf("failed to seek to start of sticker zip: %w", err)
|
||||
} else if zipData, err := io.ReadAll(f); err != nil {
|
||||
return nil, fmt.Errorf("failed to read sticker zip: %w", err)
|
||||
} else if data, _, err := msgconv.ExtractAnimatedSticker(zipData); err != nil {
|
||||
return nil, fmt.Errorf("failed to extract animated sticker: %w %x", err, zipData)
|
||||
} else if _, err := f.WriteAt(data, 0); err != nil {
|
||||
return nil, fmt.Errorf("failed to write animated sticker to file: %w", err)
|
||||
} else if err := f.Truncate(int64(len(data))); err != nil {
|
||||
return nil, fmt.Errorf("failed to truncate animated sticker file: %w", err)
|
||||
}
|
||||
mimeType = "video/lottie+json"
|
||||
}
|
||||
|
||||
return &mediaproxy.FileMeta{
|
||||
ContentType: mimeType,
|
||||
}, nil
|
||||
return nil
|
||||
},
|
||||
// TODO?
|
||||
ContentType: "",
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
@ -285,16 +160,12 @@ func (wa *WhatsAppClient) requestAndWaitDirectMedia(ctx context.Context, rawMsgI
|
|||
}
|
||||
switch state.resultType {
|
||||
case waMmsRetry.MediaRetryNotification_NOT_FOUND:
|
||||
return mautrix.MNotFound.WithMessage("This media was not found on your phone.")
|
||||
case waMmsRetry.MediaRetryNotification_DECRYPTION_ERROR:
|
||||
return mautrix.MNotFound.WithMessage("Unable to retrieve media: phone reported a decryption error. The original message may have been deleted.")
|
||||
case waMmsRetry.MediaRetryNotification_GENERAL_ERROR:
|
||||
return mautrix.MNotFound.WithMessage("Unable to retrieve media: phone returned an error. Please ensure your phone is connected to the internet and WhatsApp is running.").WithCanRetry(true)
|
||||
return mautrix.MNotFound.WithMessage("Media not found on phone")
|
||||
default:
|
||||
return mautrix.MNotFound.WithMessage(fmt.Sprintf("Unable to retrieve media: phone returned error code %d", state.resultType)).WithCanRetry(true)
|
||||
return mautrix.MNotFound.WithMessage("Phone returned error response")
|
||||
}
|
||||
case <-time.After(30 * time.Second):
|
||||
return mautrix.MNotFound.WithMessage("Phone did not respond in time. Please ensure your phone is connected to the internet and WhatsApp is open.").WithStatus(http.StatusGatewayTimeout).WithCanRetry(true)
|
||||
return mautrix.MNotFound.WithMessage("Phone did not respond in time").WithStatus(http.StatusGatewayTimeout)
|
||||
case <-ctx.Done():
|
||||
return ctx.Err()
|
||||
}
|
||||
|
|
@ -306,7 +177,7 @@ func (wa *WhatsAppClient) requestDirectMedia(ctx context.Context, rawMsgID netwo
|
|||
defer state.Unlock()
|
||||
if !state.requested {
|
||||
zerolog.Ctx(ctx).Debug().Msg("Sending request for missing media in direct download")
|
||||
err := wa.sendMediaRequestDirect(ctx, rawMsgID, key)
|
||||
err := wa.sendMediaRequestDirect(rawMsgID, key)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to send media retry request: %w", err)
|
||||
}
|
||||
|
|
@ -338,9 +209,7 @@ func (wa *WhatsAppClient) receiveDirectMediaRetry(ctx context.Context, msg *data
|
|||
log.Warn().Err(err).Msg("Failed to decrypt media retry notification")
|
||||
return
|
||||
}
|
||||
if state != nil {
|
||||
state.resultType = retryData.GetResult()
|
||||
}
|
||||
if retryData.GetResult() != waMmsRetry.MediaRetryNotification_SUCCESS {
|
||||
errorName := waMmsRetry.MediaRetryNotification_ResultType_name[int32(retryData.GetResult())]
|
||||
if retryData.GetDirectPath() == "" {
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ func (evt *MessageInfoWrapper) GetTimestamp() time.Time {
|
|||
}
|
||||
|
||||
func (evt *MessageInfoWrapper) GetSender() bridgev2.EventSender {
|
||||
return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, evt.Info.Sender)
|
||||
return evt.wa.makeEventSender(evt.Info.Sender)
|
||||
}
|
||||
|
||||
func (evt *MessageInfoWrapper) GetID() networkid.MessageID {
|
||||
|
|
@ -124,50 +124,6 @@ func (evt *WAMessageEvent) AddLogContext(c zerolog.Context) zerolog.Context {
|
|||
return evt.MessageInfoWrapper.AddLogContext(c).Str("parsed_message_type", evt.parsedMessageType)
|
||||
}
|
||||
|
||||
func (evt *WAMessageEvent) PreHandle(ctx context.Context, portal *bridgev2.Portal) {
|
||||
if evt.Info.AddressingMode != types.AddressingModeLID || evt.Info.Chat.Server != types.GroupServer {
|
||||
return
|
||||
}
|
||||
portalJID, err := waid.ParsePortalID(portal.ID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
meta := portal.Metadata.(*waid.PortalMetadata)
|
||||
if meta.AddressingMode == types.AddressingModeLID && evt.Info.Sender.Server == types.DefaultUserServer {
|
||||
evt.Info.Sender, evt.Info.SenderAlt = evt.Info.SenderAlt, evt.Info.Sender
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Stringer("lid", evt.Info.Sender).
|
||||
Stringer("pn", evt.Info.SenderAlt).
|
||||
Str("message_id", evt.Info.ID).
|
||||
Msg("Forced phone number sender to LID in group message")
|
||||
}
|
||||
if meta.AddressingMode == types.AddressingModeLID || meta.LIDMigrationAttempted {
|
||||
return
|
||||
}
|
||||
log := zerolog.Ctx(ctx).With().Str("action", "group lid migration").Logger()
|
||||
ctx = log.WithContext(ctx)
|
||||
meta.LIDMigrationAttempted = true
|
||||
info, err := evt.wa.Client.GetGroupInfo(ctx, portalJID)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to get group info for lid migration")
|
||||
return
|
||||
}
|
||||
if info.AddressingMode != types.AddressingModeLID {
|
||||
log.Warn().Msg("Received LID message, but group addressing mode isn't set to LID? Not migrating")
|
||||
return
|
||||
}
|
||||
log.Info().Msg("Resyncing group members as it appears to have switched to LID addressing mode")
|
||||
portal.UpdateInfo(ctx, evt.wa.wrapGroupInfo(ctx, info), evt.wa.UserLogin, nil, time.Time{})
|
||||
log.Debug().Msg("Finished resyncing after LID change")
|
||||
if evt.Info.Sender.Server == types.DefaultUserServer && evt.Info.SenderAlt.Server == types.HiddenUserServer {
|
||||
evt.Info.Sender, evt.Info.SenderAlt = evt.Info.SenderAlt, evt.Info.Sender
|
||||
log.Debug().
|
||||
Stringer("new_sender", evt.Info.Sender).
|
||||
Stringer("new_sender_alt", evt.Info.SenderAlt).
|
||||
Msg("Overriding sender to LID after resyncing group members")
|
||||
}
|
||||
}
|
||||
|
||||
func (evt *WAMessageEvent) PostHandle(ctx context.Context, portal *bridgev2.Portal) {
|
||||
if ph := evt.postHandle; ph != nil {
|
||||
evt.postHandle = nil
|
||||
|
|
@ -194,10 +150,7 @@ func (evt *WAMessageEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Por
|
|||
meta.Edits = append(meta.Edits, evt.Info.ID)
|
||||
}
|
||||
|
||||
ctx = context.WithValue(ctx, msgconv.ContextKeyEditTargetID, evt.Message.GetProtocolMessage().GetKey().GetID())
|
||||
cm := evt.wa.Main.MsgConv.ToMatrix(
|
||||
ctx, portal, evt.wa.Client, intent, editedMsg, evt.MsgEvent.RawMessage, &evt.Info, evt.isViewOnce(), false, previouslyConvertedPart,
|
||||
)
|
||||
cm := evt.wa.Main.MsgConv.ToMatrix(ctx, portal, evt.wa.Client, intent, editedMsg, &evt.Info, evt.isViewOnce(), previouslyConvertedPart)
|
||||
if evt.isUndecryptableUpsertSubEvent && isFailedMedia(cm) {
|
||||
evt.postHandle = func() {
|
||||
evt.wa.processFailedMedia(ctx, portal.PortalKey, evt.GetID(), cm, false)
|
||||
|
|
@ -217,15 +170,9 @@ func (evt *WAMessageEvent) ConvertEdit(ctx context.Context, portal *bridgev2.Por
|
|||
|
||||
func (evt *WAMessageEvent) GetTargetMessage() networkid.MessageID {
|
||||
if reactionMsg := evt.Message.GetReactionMessage(); reactionMsg != nil {
|
||||
ctx := evt.wa.UserLogin.Log.
|
||||
With().Str("action", "get reaction target message").Str("message_id", evt.Info.ID).Logger().
|
||||
WithContext(evt.wa.Main.Bridge.BackgroundCtx)
|
||||
return msgconv.KeyToMessageID(ctx, evt.wa.Client, evt.Info.Chat, evt.Info.Sender, reactionMsg.GetKey())
|
||||
return msgconv.KeyToMessageID(evt.wa.Client, evt.Info.Chat, evt.Info.Sender, reactionMsg.GetKey())
|
||||
} else if protocolMsg := evt.Message.GetProtocolMessage(); protocolMsg != nil {
|
||||
ctx := evt.wa.UserLogin.Log.
|
||||
With().Str("action", "get edit target message").Str("message_id", evt.Info.ID).Logger().
|
||||
WithContext(evt.wa.Main.Bridge.BackgroundCtx)
|
||||
return msgconv.KeyToMessageID(ctx, evt.wa.Client, evt.Info.Chat, evt.Info.Sender, protocolMsg.GetKey())
|
||||
return msgconv.KeyToMessageID(evt.wa.Client, evt.Info.Chat, evt.Info.Sender, protocolMsg.GetKey())
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
|
@ -277,10 +224,8 @@ func (evt *WAMessageEvent) HandleExisting(ctx context.Context, portal *bridgev2.
|
|||
}
|
||||
|
||||
func (evt *WAMessageEvent) ConvertMessage(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI) (*bridgev2.ConvertedMessage, error) {
|
||||
evt.wa.EnqueuePortalResync(portal, false)
|
||||
converted := evt.wa.Main.MsgConv.ToMatrix(
|
||||
ctx, portal, evt.wa.Client, intent, evt.Message, evt.MsgEvent.RawMessage, &evt.Info, evt.isViewOnce(), false, nil,
|
||||
)
|
||||
evt.wa.EnqueuePortalResync(portal)
|
||||
converted := evt.wa.Main.MsgConv.ToMatrix(ctx, portal, evt.wa.Client, intent, evt.Message, &evt.Info, evt.isViewOnce(), nil)
|
||||
if isFailedMedia(converted) {
|
||||
evt.postHandle = func() {
|
||||
evt.wa.processFailedMedia(ctx, portal.PortalKey, evt.GetID(), converted, false)
|
||||
|
|
@ -354,13 +299,9 @@ func (evt *WAUndecryptableMessage) ConvertMessage(ctx context.Context, portal *b
|
|||
}
|
||||
content := &undecryptableMessageContent
|
||||
if evt.Type == events.UnavailableTypeViewOnce {
|
||||
body := "You received a view once message. For added privacy, you can only open it on the WhatsApp app."
|
||||
if evt.Info.IsFromMe {
|
||||
body = "You sent a view once message from another device."
|
||||
}
|
||||
content = &event.MessageEventContent{
|
||||
MsgType: event.MsgNotice,
|
||||
Body: body,
|
||||
Body: "You received a view once message. For added privacy, you can only open it on the WhatsApp app.",
|
||||
}
|
||||
}
|
||||
// TODO thread root for comments
|
||||
|
|
@ -416,7 +357,7 @@ func (evt *WAMediaRetry) getRealSender() types.JID {
|
|||
}
|
||||
|
||||
func (evt *WAMediaRetry) GetSender() bridgev2.EventSender {
|
||||
return evt.wa.makeEventSender(evt.wa.Main.Bridge.BackgroundCtx, evt.getRealSender())
|
||||
return evt.wa.makeEventSender(evt.getRealSender())
|
||||
}
|
||||
|
||||
func (evt *WAMediaRetry) GetTargetMessage() networkid.MessageID {
|
||||
|
|
|
|||
|
|
@ -16,9 +16,8 @@ proxy_only_login: false
|
|||
# {{.PushName}} - nickname set by the WhatsApp user
|
||||
# {{.BusinessName}} - validated WhatsApp business name
|
||||
# {{.Phone}} - phone number (international format)
|
||||
# {{.RedactedPhone}} - phone number with middle digits replaced by "∙"
|
||||
# {{.FullName}} - Name you set in the contacts list
|
||||
displayname_template: '{{or .BusinessName .PushName .Phone .RedactedPhone "Unknown user"}} (WA)'
|
||||
displayname_template: "{{or .BusinessName .PushName .Phone}} (WA)"
|
||||
|
||||
# Should incoming calls send a message to the Matrix room?
|
||||
call_start_notices: true
|
||||
|
|
@ -62,13 +61,6 @@ force_active_delivery_receipts: false
|
|||
# When direct media is enabled and a piece of media isn't available on the WhatsApp servers,
|
||||
# should it be automatically requested from the phone?
|
||||
direct_media_auto_request: true
|
||||
# Should the bridge automatically reconnect if it fails to connect on startup?
|
||||
initial_auto_reconnect: true
|
||||
# WhatsApp messages are sometimes undecryptable. Should the bridge store messages it sends in the
|
||||
# bridge database in order to accept retry receipts from other WhatsApp users for messages sent via
|
||||
# the bridge? By default, the bridge only stores messages in memory, and therefore can't accept
|
||||
# retry receipts if the bridge is restarted after the message is sent.
|
||||
use_whatsapp_retry_store: false
|
||||
|
||||
# Settings for converting animated stickers.
|
||||
animated_sticker:
|
||||
|
|
@ -94,10 +86,6 @@ history_sync:
|
|||
# Should the bridge request a full sync from the phone when logging in?
|
||||
# This bumps the size of history syncs from 3 months to 1 year.
|
||||
request_full_sync: false
|
||||
# Time to wait for history sync payloads before starting backfill. Each new payload resets the timer.
|
||||
# If this is too low, the backfill may happen with incomplete history
|
||||
# and backfill less messages than what is configured in the backfill section.
|
||||
dispatch_wait: 1m
|
||||
# Configuration parameters that are sent to the phone along with the request full sync flag.
|
||||
# By default, (when the values are null or 0), the config isn't sent at all.
|
||||
full_sync_config:
|
||||
|
|
@ -121,6 +109,3 @@ history_sync:
|
|||
request_local_time: 120
|
||||
# Maximum number of media request responses to handle in parallel per user.
|
||||
max_async_handle: 2
|
||||
# Use on-demand history sync requests for fetching older messages?
|
||||
# This only applies when using the backfill queue, never for forward backfills.
|
||||
backwards_on_demand: false
|
||||
|
|
|
|||
|
|
@ -1,25 +1,17 @@
|
|||
package connector
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/jpeg"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/ptr"
|
||||
"go.mau.fi/util/variationselector"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/appstate"
|
||||
"go.mau.fi/whatsmeow/proto/waCommon"
|
||||
"go.mau.fi/whatsmeow/proto/waE2E"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"golang.org/x/image/draw"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/database"
|
||||
|
|
@ -37,15 +29,6 @@ var (
|
|||
_ bridgev2.RedactionHandlingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.ReadReceiptHandlingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.PollHandlingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.DisappearTimerChangingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.MembershipHandlingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.RoomNameHandlingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.RoomTopicHandlingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.RoomAvatarHandlingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.MuteHandlingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.TagHandlingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.MarkedUnreadHandlingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.DeleteChatHandlingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
)
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixPollStart(ctx context.Context, msg *bridgev2.MatrixPollStart) (*bridgev2.MatrixMessageResponse, error) {
|
||||
|
|
@ -90,12 +73,7 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg
|
|||
if req == nil {
|
||||
req = &whatsmeow.SendRequestExtra{}
|
||||
}
|
||||
if strings.HasPrefix(string(msg.InputTransactionID), whatsmeow.WebMessageIDPrefix) {
|
||||
req.ID = types.MessageID(msg.InputTransactionID)
|
||||
} else {
|
||||
req.ID = wa.Client.GenerateMessageID()
|
||||
}
|
||||
|
||||
chatJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
@ -104,16 +82,15 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg
|
|||
return nil, ErrBroadcastSendDisabled
|
||||
}
|
||||
wrappedMsgID := waid.MakeMessageID(chatJID, wa.JID, req.ID)
|
||||
wrappedMsgID2 := waid.MakeMessageID(chatJID, wa.GetStore().GetLID(), req.ID)
|
||||
wrappedMsgID2 := waid.MakeMessageID(chatJID, wa.Device.GetLID(), req.ID)
|
||||
msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID))
|
||||
msg.AddPendingToIgnore(networkid.TransactionID(wrappedMsgID2))
|
||||
zerolog.Ctx(ctx).Trace().Any("payload", waMsg).Msg("Outgoing message payload")
|
||||
resp, err := wa.Client.SendMessage(ctx, chatJID, waMsg, *req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var pickedMessageID networkid.MessageID
|
||||
if resp.Sender == wa.GetStore().GetLID() && chatJID.Server != types.DefaultUserServer {
|
||||
if resp.Sender == wa.Device.GetLID() {
|
||||
pickedMessageID = wrappedMsgID2
|
||||
msg.RemovePending(networkid.TransactionID(wrappedMsgID))
|
||||
} else {
|
||||
|
|
@ -137,7 +114,7 @@ func (wa *WhatsAppClient) handleConvertedMatrixMessage(ctx context.Context, msg
|
|||
func (wa *WhatsAppClient) PreHandleMatrixReaction(_ context.Context, msg *bridgev2.MatrixReaction) (bridgev2.MatrixReactionPreResponse, error) {
|
||||
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return bridgev2.MatrixReactionPreResponse{}, fmt.Errorf("failed to parse portal ID: %w", err)
|
||||
return bridgev2.MatrixReactionPreResponse{}, err
|
||||
} else if portalJID == types.StatusBroadcastJID {
|
||||
return bridgev2.MatrixReactionPreResponse{}, ErrBroadcastReactionUnsupported
|
||||
}
|
||||
|
|
@ -145,7 +122,7 @@ func (wa *WhatsAppClient) PreHandleMatrixReaction(_ context.Context, msg *bridge
|
|||
if portalJID.Server == types.HiddenUserServer ||
|
||||
msg.Portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup ||
|
||||
msg.Portal.Metadata.(*waid.PortalMetadata).AddressingMode == types.AddressingModeLID {
|
||||
sender = wa.GetStore().GetLID()
|
||||
sender = wa.Device.GetLID()
|
||||
}
|
||||
return bridgev2.MatrixReactionPreResponse{
|
||||
SenderID: waid.MakeUserID(sender),
|
||||
|
|
@ -157,12 +134,12 @@ func (wa *WhatsAppClient) PreHandleMatrixReaction(_ context.Context, msg *bridge
|
|||
func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev2.MatrixReaction) (*database.Reaction, error) {
|
||||
messageID, err := waid.ParseMessageID(msg.TargetMessage.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse target message ID: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse portal ID: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
reactionMsg := &waE2E.Message{
|
||||
ReactionMessage: &waE2E.ReactionMessage{
|
||||
|
|
@ -173,7 +150,7 @@ func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev
|
|||
}
|
||||
var req whatsmeow.SendRequestExtra
|
||||
if msg.Portal.Metadata.(*waid.PortalMetadata).CommunityAnnouncementGroup {
|
||||
reactionMsg.EncReactionMessage, err = wa.Client.EncryptReaction(ctx, msgconv.MessageIDToInfo(wa.Client, messageID), reactionMsg.ReactionMessage)
|
||||
reactionMsg.EncReactionMessage, err = wa.Client.EncryptReaction(msgconv.MessageIDToInfo(wa.Client, messageID), reactionMsg.ReactionMessage)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to encrypt reaction: %w", err)
|
||||
}
|
||||
|
|
@ -195,12 +172,12 @@ func (wa *WhatsAppClient) HandleMatrixReaction(ctx context.Context, msg *bridgev
|
|||
func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *bridgev2.MatrixReactionRemove) error {
|
||||
messageID, err := waid.ParseMessageID(msg.TargetReaction.MessageID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse target message ID: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse portal ID: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
reactionMsg := &waE2E.Message{
|
||||
|
|
@ -211,34 +188,23 @@ func (wa *WhatsAppClient) HandleMatrixReactionRemove(ctx context.Context, msg *b
|
|||
},
|
||||
}
|
||||
|
||||
extra := whatsmeow.SendRequestExtra{}
|
||||
if strings.HasPrefix(string(msg.InputTransactionID), whatsmeow.WebMessageIDPrefix) {
|
||||
extra.ID = types.MessageID(msg.InputTransactionID)
|
||||
}
|
||||
|
||||
resp, err := wa.Client.SendMessage(ctx, portalJID, reactionMsg, extra)
|
||||
resp, err := wa.Client.SendMessage(ctx, portalJID, reactionMsg)
|
||||
zerolog.Ctx(ctx).Trace().Any("response", resp).Msg("WhatsApp reaction response")
|
||||
return err
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixEdit(ctx context.Context, edit *bridgev2.MatrixEdit) error {
|
||||
log := zerolog.Ctx(ctx)
|
||||
|
||||
var editID types.MessageID
|
||||
if strings.HasPrefix(string(edit.InputTransactionID), whatsmeow.WebMessageIDPrefix) {
|
||||
editID = types.MessageID(edit.InputTransactionID)
|
||||
} else {
|
||||
editID = wa.Client.GenerateMessageID()
|
||||
}
|
||||
editID := wa.Client.GenerateMessageID()
|
||||
|
||||
messageID, err := waid.ParseMessageID(edit.EditTarget.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse target message ID: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
portalJID, err := waid.ParsePortalID(edit.Portal.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse portal ID: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
waMsg, _, err := wa.Main.MsgConv.ToWhatsApp(ctx, wa.Client, edit.Event, edit.Content, nil, nil, edit.Portal)
|
||||
|
|
@ -263,22 +229,17 @@ func (wa *WhatsAppClient) HandleMatrixMessageRemove(ctx context.Context, msg *br
|
|||
log := zerolog.Ctx(ctx)
|
||||
messageID, err := waid.ParseMessageID(msg.TargetMessage.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse target message ID: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse portal ID: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
revokeMessage := wa.Client.BuildRevoke(messageID.Chat, messageID.Sender, messageID.ID)
|
||||
|
||||
extra := whatsmeow.SendRequestExtra{}
|
||||
if strings.HasPrefix(string(msg.InputTransactionID), whatsmeow.WebMessageIDPrefix) {
|
||||
extra.ID = types.MessageID(msg.InputTransactionID)
|
||||
}
|
||||
|
||||
resp, err := wa.Client.SendMessage(ctx, portalJID, revokeMessage, extra)
|
||||
resp, err := wa.Client.SendMessage(ctx, portalJID, revokeMessage)
|
||||
log.Trace().Any("response", resp).Msg("WhatsApp delete response")
|
||||
return err
|
||||
}
|
||||
|
|
@ -292,7 +253,7 @@ func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *
|
|||
}
|
||||
portalJID, err := waid.ParsePortalID(receipt.Portal.ID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse portal ID: %w", err)
|
||||
return err
|
||||
}
|
||||
messages, err := receipt.Portal.Bridge.DB.Message.GetMessagesBetweenTimeQuery(ctx, receipt.Portal.PortalKey, receipt.LastRead, receipt.ReadUpTo)
|
||||
if err != nil {
|
||||
|
|
@ -312,7 +273,7 @@ func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *
|
|||
if err != nil {
|
||||
continue
|
||||
}
|
||||
if parsed.Sender.User == wa.GetStore().GetLID().User || parsed.Sender.User == wa.JID.User {
|
||||
if parsed.Sender.User == wa.Device.GetLID().User || parsed.Sender.User == wa.JID.User {
|
||||
continue
|
||||
}
|
||||
var key types.JID
|
||||
|
|
@ -323,7 +284,7 @@ func (wa *WhatsAppClient) HandleMatrixReadReceipt(ctx context.Context, receipt *
|
|||
messagesToRead[key] = append(messagesToRead[key], parsed.ID)
|
||||
}
|
||||
for messageSender, ids := range messagesToRead {
|
||||
err = wa.Client.MarkRead(ctx, ids, receipt.Receipt.Timestamp, portalJID, messageSender)
|
||||
err = wa.Client.MarkRead(ids, receipt.Receipt.Timestamp, portalJID, messageSender)
|
||||
if err != nil {
|
||||
log.Err(err).Strs("ids", ids).Msg("Failed to mark messages as read")
|
||||
}
|
||||
|
|
@ -353,60 +314,26 @@ func (wa *WhatsAppClient) HandleMatrixTyping(ctx context.Context, msg *bridgev2.
|
|||
}
|
||||
|
||||
if wa.Main.Config.SendPresenceOnTyping {
|
||||
err = wa.updatePresence(ctx, types.PresenceAvailable)
|
||||
err = wa.Client.SendPresence(types.PresenceAvailable)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to set presence on typing")
|
||||
}
|
||||
}
|
||||
return wa.Client.SendChatPresence(ctx, portalJID, chatPresence, mediaPresence)
|
||||
return wa.Client.SendChatPresence(portalJID, chatPresence, mediaPresence)
|
||||
}
|
||||
|
||||
var errUnsupportedDisappearingTimer = bridgev2.WrapErrorInStatus(errors.New("invalid value for disappearing timer")).WithErrorAsMessage().WithIsCertain(true).WithSendNotice(true)
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixDisappearingTimer(ctx context.Context, msg *bridgev2.MatrixDisappearingTimer) (bool, error) {
|
||||
func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (bool, error) {
|
||||
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
switch msg.Content.Timer.Duration {
|
||||
case whatsmeow.DisappearingTimerOff, whatsmeow.DisappearingTimer24Hours, whatsmeow.DisappearingTimer7Days, whatsmeow.DisappearingTimer90Days:
|
||||
default:
|
||||
return false, fmt.Errorf("%w (%s)", errUnsupportedDisappearingTimer, msg.Content.Timer.Duration)
|
||||
}
|
||||
|
||||
settingTS := time.UnixMilli(msg.Event.Timestamp)
|
||||
err = wa.Client.SetDisappearingTimer(ctx, portalJID, msg.Content.Timer.Duration, settingTS)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
msg.Portal.Metadata.(*waid.PortalMetadata).DisappearingTimerSetAt = settingTS.Unix()
|
||||
msg.Portal.Disappear = database.DisappearingSetting{
|
||||
Type: event.DisappearingTypeAfterSend,
|
||||
Timer: msg.Content.Timer.Duration,
|
||||
}
|
||||
if msg.Portal.Disappear.Timer == 0 {
|
||||
msg.Portal.Disappear.Type = event.DisappearingTypeNone
|
||||
}
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridgev2.MatrixMembershipChange) (*bridgev2.MatrixMembershipResult, error) {
|
||||
if msg.Type.IsSelf && msg.OrigSender != nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if msg.Portal.RoomType == database.RoomTypeDM {
|
||||
switch msg.Type {
|
||||
case bridgev2.Invite:
|
||||
return nil, fmt.Errorf("cannot invite additional user to dm")
|
||||
return false, fmt.Errorf("cannot invite additional user to dm")
|
||||
default:
|
||||
return nil, nil
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -419,7 +346,7 @@ func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridg
|
|||
case bridgev2.Leave, bridgev2.Kick:
|
||||
action = whatsmeow.ParticipantChangeRemove
|
||||
default:
|
||||
return nil, nil
|
||||
return false, nil
|
||||
}
|
||||
|
||||
switch target := msg.Target.(type) {
|
||||
|
|
@ -428,245 +355,17 @@ func (wa *WhatsAppClient) HandleMatrixMembership(ctx context.Context, msg *bridg
|
|||
case *bridgev2.UserLogin:
|
||||
ghost, err := target.Bridge.GetGhostByID(ctx, networkid.UserID(target.ID))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get ghost for user: %w", err)
|
||||
return false, fmt.Errorf("failed to get ghost for user: %w", err)
|
||||
}
|
||||
changes[0] = waid.ParseUserID(ghost.ID)
|
||||
default:
|
||||
return nil, fmt.Errorf("cannot get target intent: unknown type: %T", target)
|
||||
return false, fmt.Errorf("cannot get target intent: unknown type: %T", target)
|
||||
}
|
||||
|
||||
resp, err := wa.Client.UpdateGroupParticipants(ctx, portalJID, changes, action)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
} else if len(resp) == 0 {
|
||||
return nil, fmt.Errorf("no response for participant change")
|
||||
} else if resp[0].Error != 0 {
|
||||
return nil, fmt.Errorf("failed to change participant: code %d", resp[0].Error)
|
||||
}
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Any("change_response", resp).
|
||||
Msg("Handled membership change")
|
||||
|
||||
return &bridgev2.MatrixMembershipResult{RedirectTo: waid.MakeUserID(resp[0].JID)}, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixRoomName(ctx context.Context, msg *bridgev2.MatrixRoomName) (bool, error) {
|
||||
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
_, err = wa.Client.UpdateGroupParticipants(portalJID, changes, action)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if msg.Portal.RoomType == database.RoomTypeDM {
|
||||
return false, fmt.Errorf("cannot set room name for DM")
|
||||
}
|
||||
|
||||
err = wa.Client.SetGroupName(ctx, portalJID, msg.Content.Name)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
msg.Portal.Name = msg.Content.Name
|
||||
msg.Portal.NameSet = true
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixRoomTopic(ctx context.Context, msg *bridgev2.MatrixRoomTopic) (bool, error) {
|
||||
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if msg.Portal.RoomType == database.RoomTypeDM {
|
||||
return false, fmt.Errorf("cannot set room topic for DM")
|
||||
}
|
||||
|
||||
newID := wa.Client.GenerateMessageID()
|
||||
oldID := msg.Portal.Metadata.(*waid.PortalMetadata).TopicID
|
||||
err = wa.Client.SetGroupTopic(ctx, portalJID, oldID, newID, msg.Content.Topic)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
msg.Portal.Topic = msg.Content.Topic
|
||||
msg.Portal.TopicSet = true
|
||||
msg.Portal.Metadata.(*waid.PortalMetadata).TopicID = newID
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixRoomAvatar(ctx context.Context, msg *bridgev2.MatrixRoomAvatar) (bool, error) {
|
||||
portalJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if msg.Portal.RoomType == database.RoomTypeDM {
|
||||
return false, fmt.Errorf("cannot set room avatar for DM")
|
||||
}
|
||||
|
||||
var data []byte
|
||||
if msg.Content.URL != "" {
|
||||
data, err = msg.Portal.Bridge.Bot.DownloadMedia(ctx, msg.Content.URL, nil)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("failed to download avatar: %w", err)
|
||||
}
|
||||
|
||||
data, err = convertRoomAvatar(data)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
avatarID, err := wa.Client.SetGroupPhoto(ctx, portalJID, data)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
msg.Portal.AvatarMXC = msg.Content.URL
|
||||
if data == nil {
|
||||
msg.Portal.AvatarHash = [32]byte{}
|
||||
msg.Portal.AvatarID = "remove"
|
||||
} else {
|
||||
msg.Portal.AvatarHash = sha256.Sum256(data)
|
||||
msg.Portal.AvatarID = networkid.AvatarID(avatarID)
|
||||
}
|
||||
msg.Portal.AvatarSet = true
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
const avatarMaxSize = 720
|
||||
const avatarMinSize = 190
|
||||
|
||||
func convertRoomAvatar(data []byte) ([]byte, error) {
|
||||
cfg, imageType, err := image.DecodeConfig(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode avatar: %w", err)
|
||||
}
|
||||
width, height := cfg.Width, cfg.Height
|
||||
isCorrectSize := width == height && avatarMinSize < width && width < avatarMaxSize
|
||||
if isCorrectSize && imageType == "jpeg" {
|
||||
return data, nil
|
||||
} else if len(data) > 10*1024*1024 || width > 12000 || height > 12000 {
|
||||
return nil, fmt.Errorf("avatar is too large for re-encoding")
|
||||
}
|
||||
|
||||
img, _, err := image.Decode(bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode avatar: %w", err)
|
||||
}
|
||||
|
||||
if !isCorrectSize {
|
||||
var squareCrop image.Rectangle
|
||||
var dstSize int
|
||||
if width > height {
|
||||
dstSize = max(avatarMinSize, min(height, avatarMaxSize))
|
||||
|
||||
offset := (width - height) / 2
|
||||
squareCrop = image.Rect(offset, 0, width-offset, height)
|
||||
} else {
|
||||
dstSize = max(avatarMinSize, min(width, avatarMaxSize))
|
||||
|
||||
offset := (height - width) / 2
|
||||
squareCrop = image.Rect(0, offset, width, height-offset)
|
||||
}
|
||||
|
||||
cropped := image.NewRGBA(image.Rect(0, 0, dstSize, dstSize))
|
||||
draw.BiLinear.Scale(cropped, cropped.Rect, img, squareCrop, draw.Src, nil)
|
||||
img = cropped
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
err = jpeg.Encode(&buf, img, &jpeg.Options{Quality: jpeg.DefaultQuality})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to re-encode avatar: %w", err)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMute(ctx context.Context, msg *bridgev2.MatrixMute) error {
|
||||
chatJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
mutedUntil := msg.Content.GetMutedUntilTime()
|
||||
muted := mutedUntil.After(time.Now())
|
||||
muteTS := ptr.Ptr(mutedUntil.UnixMilli())
|
||||
if !muted || mutedUntil == event.MutedForever {
|
||||
muteTS = nil
|
||||
}
|
||||
return wa.Client.SendAppState(ctx, appstate.BuildMuteAbs(chatJID, muted, muteTS))
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleRoomTag(ctx context.Context, msg *bridgev2.MatrixRoomTag) error {
|
||||
chatJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, isFavorite := msg.Content.Tags[event.RoomTagFavourite]
|
||||
return wa.Client.SendAppState(ctx, appstate.BuildPin(chatJID, isFavorite))
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) getLastMessageInfo(ctx context.Context, chatJID types.JID, portalKey networkid.PortalKey) (time.Time, *waCommon.MessageKey, error) {
|
||||
msgs, err := wa.Main.Bridge.DB.Message.GetLastNInPortal(ctx, portalKey, 1)
|
||||
if err != nil {
|
||||
return time.Time{}, nil, fmt.Errorf("failed to get last message in portal: %w", err)
|
||||
}
|
||||
var lastTS time.Time
|
||||
var lastKey *waCommon.MessageKey
|
||||
if len(msgs) == 1 {
|
||||
lastTS = msgs[0].Timestamp
|
||||
parsed, _ := waid.ParseMessageID(msgs[0].ID)
|
||||
if parsed != nil {
|
||||
fromMe := parsed.Sender.ToNonAD() == wa.JID.ToNonAD() || parsed.Sender.ToNonAD() == wa.GetStore().GetLID().ToNonAD()
|
||||
var participant *string
|
||||
if chatJID.Server == types.GroupServer {
|
||||
participant = ptr.Ptr(parsed.Sender.String())
|
||||
}
|
||||
lastKey = &waCommon.MessageKey{
|
||||
RemoteJID: ptr.Ptr(chatJID.String()),
|
||||
FromMe: &fromMe,
|
||||
ID: &parsed.ID,
|
||||
Participant: participant,
|
||||
}
|
||||
}
|
||||
}
|
||||
return lastTS, lastKey, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMarkedUnread(ctx context.Context, msg *bridgev2.MatrixMarkedUnread) error {
|
||||
chatJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
lastTS, lastKey, err := wa.getLastMessageInfo(ctx, chatJID, msg.Portal.PortalKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return wa.Client.SendAppState(ctx, appstate.BuildMarkChatAsRead(chatJID, msg.Content.Unread, lastTS, lastKey))
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) HandleMatrixDeleteChat(ctx context.Context, msg *bridgev2.MatrixDeleteChat) error {
|
||||
chatJID, err := waid.ParsePortalID(msg.Portal.ID)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if chatJID.Server == types.GroupServer {
|
||||
memberInfo, err := wa.Main.Bridge.Matrix.GetMemberInfo(ctx, msg.Portal.MXID, wa.UserLogin.UserMXID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get own member info: %w", err)
|
||||
} else if memberInfo.Membership == event.MembershipJoin {
|
||||
err = wa.Client.LeaveGroup(ctx, chatJID)
|
||||
if err != nil {
|
||||
// TODO ignore errors saying you already left the group?
|
||||
return fmt.Errorf("failed to leave group before deleting chat: %w", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
lastTS, lastKey, err := wa.getLastMessageInfo(ctx, chatJID, msg.Portal.PortalKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return wa.Client.SendAppState(ctx, appstate.BuildDeleteChat(chatJID, lastTS, lastKey, true))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,10 +25,7 @@ import (
|
|||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/ptr"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/appstate"
|
||||
"go.mau.fi/whatsmeow/proto/waE2E"
|
||||
"go.mau.fi/whatsmeow/store"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"go.mau.fi/whatsmeow/types/events"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
|
|
@ -74,77 +71,78 @@ func init() {
|
|||
})
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
|
||||
func (wa *WhatsAppClient) handleWAEvent(rawEvt any) {
|
||||
log := wa.UserLogin.Log
|
||||
ctx := log.WithContext(wa.Main.Bridge.BackgroundCtx)
|
||||
|
||||
success = true
|
||||
switch evt := rawEvt.(type) {
|
||||
case *events.Message:
|
||||
success = wa.handleWAMessage(ctx, evt)
|
||||
wa.handleWAMessage(evt)
|
||||
case *events.Receipt:
|
||||
success = wa.handleWAReceipt(ctx, evt)
|
||||
wa.handleWAReceipt(evt)
|
||||
case *events.ChatPresence:
|
||||
wa.handleWAChatPresence(ctx, evt)
|
||||
wa.handleWAChatPresence(evt)
|
||||
case *events.UndecryptableMessage:
|
||||
success = wa.handleWAUndecryptableMessage(ctx, evt)
|
||||
wa.handleWAUndecryptableMessage(evt)
|
||||
|
||||
case *events.CallOffer:
|
||||
success = wa.handleWACallStart(ctx, evt.GroupJID, evt.CallCreator, evt.CallCreatorAlt, evt.CallID, "", evt.Timestamp)
|
||||
wa.handleWACallStart(evt.CallCreator, evt.CallID, "", evt.Timestamp)
|
||||
case *events.CallOfferNotice:
|
||||
success = wa.handleWACallStart(ctx, evt.GroupJID, evt.CallCreator, evt.CallCreatorAlt, evt.CallID, evt.Type, evt.Timestamp)
|
||||
wa.handleWACallStart(evt.CallCreator, evt.CallID, evt.Type, evt.Timestamp)
|
||||
case *events.CallTerminate, *events.CallRelayLatency, *events.CallAccept, *events.UnknownCallEvent:
|
||||
// ignore
|
||||
case *events.IdentityChange:
|
||||
wa.handleWAIdentityChange(ctx, evt)
|
||||
wa.handleWAIdentityChange(evt)
|
||||
case *events.MarkChatAsRead:
|
||||
success = wa.handleWAMarkChatAsRead(ctx, evt)
|
||||
wa.handleWAMarkChatAsRead(evt)
|
||||
case *events.DeleteForMe:
|
||||
success = wa.handleWADeleteForMe(ctx, evt)
|
||||
wa.handleWADeleteForMe(evt)
|
||||
case *events.DeleteChat:
|
||||
success = wa.handleWADeleteChat(ctx, evt)
|
||||
wa.handleWADeleteChat(evt)
|
||||
case *events.Mute:
|
||||
success = wa.handleWAMute(evt)
|
||||
wa.handleWAMute(evt)
|
||||
case *events.Archive:
|
||||
success = wa.handleWAArchive(evt)
|
||||
wa.handleWAArchive(evt)
|
||||
case *events.Pin:
|
||||
success = wa.handleWAPin(evt)
|
||||
wa.handleWAPin(evt)
|
||||
|
||||
case *events.HistorySync:
|
||||
wa.UserLogin.Log.Warn().Msg("Unexpected history sync event received")
|
||||
if wa.Main.Bridge.Config.Backfill.Enabled {
|
||||
wa.historySyncs <- evt.Data
|
||||
}
|
||||
case *events.MediaRetry:
|
||||
wa.phoneSeen(evt.Timestamp)
|
||||
success = wa.UserLogin.QueueRemoteEvent(&WAMediaRetry{MediaRetry: evt, wa: wa}).Success
|
||||
wa.UserLogin.QueueRemoteEvent(&WAMediaRetry{MediaRetry: evt, wa: wa})
|
||||
|
||||
case *events.GroupInfo:
|
||||
success = wa.handleWAGroupInfoChange(ctx, evt)
|
||||
wa.handleWAGroupInfoChange(evt)
|
||||
case *events.JoinedGroup:
|
||||
success = wa.handleWAJoinedGroup(ctx, evt)
|
||||
wa.handleWAJoinedGroup(evt)
|
||||
case *events.NewsletterJoin:
|
||||
success = wa.handleWANewsletterJoin(ctx, evt)
|
||||
wa.handleWANewsletterJoin(evt)
|
||||
case *events.NewsletterLeave:
|
||||
success = wa.handleWANewsletterLeave(evt)
|
||||
wa.handleWANewsletterLeave(evt)
|
||||
case *events.Picture:
|
||||
success = wa.handleWAPictureUpdate(ctx, evt)
|
||||
go wa.handleWAPictureUpdate(evt)
|
||||
|
||||
case *events.AppStateSyncComplete:
|
||||
wa.handleWAAppStateSyncComplete(ctx, evt)
|
||||
case *events.AppStateSyncError:
|
||||
wa.handleWAAppStateSyncError(ctx, evt)
|
||||
if len(wa.GetStore().PushName) > 0 && evt.Name == appstate.WAPatchCriticalBlock {
|
||||
err := wa.Client.SendPresence(types.PresenceUnavailable)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to send presence after app state sync")
|
||||
}
|
||||
} else if evt.Name == appstate.WAPatchCriticalUnblockLow {
|
||||
go wa.resyncContacts(false)
|
||||
}
|
||||
case *events.AppState:
|
||||
// Intentionally ignored
|
||||
case *events.PushNameSetting:
|
||||
// Send presence available when connecting and when the pushname is changed.
|
||||
// This makes sure that outgoing messages always have the right pushname.
|
||||
err := wa.updatePresence(ctx, types.PresenceUnavailable)
|
||||
err := wa.Client.SendPresence(types.PresenceUnavailable)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to send presence after push name update")
|
||||
}
|
||||
_, _, err = wa.GetStore().Contacts.PutPushName(ctx, wa.JID.ToNonAD(), evt.Action.GetName())
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to update push name in store")
|
||||
}
|
||||
_, _, err = wa.GetStore().Contacts.PutPushName(ctx, wa.GetStore().GetLID().ToNonAD(), evt.Action.GetName())
|
||||
_, _, err = wa.GetStore().Contacts.PutPushName(wa.JID.ToNonAD(), evt.Action.GetName())
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to update push name in store")
|
||||
}
|
||||
|
|
@ -161,14 +159,26 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
|
|||
wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
|
||||
if len(wa.GetStore().PushName) > 0 {
|
||||
go func() {
|
||||
err := wa.updatePresence(ctx, types.PresenceUnavailable)
|
||||
err := wa.Client.SendPresence(types.PresenceUnavailable)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to send initial presence after connecting")
|
||||
}
|
||||
}()
|
||||
go wa.syncRemoteProfile(ctx, nil)
|
||||
}
|
||||
wa.MC.OnConnect(store.GetWAVersion()[2], wa.Device.Platform)
|
||||
meta := wa.UserLogin.Metadata.(*waid.UserLoginMetadata)
|
||||
if meta.WALID == "" {
|
||||
meta.WALID = wa.Client.Store.GetLID().User
|
||||
if meta.WALID != "" {
|
||||
go func() {
|
||||
err := wa.UserLogin.Save(log.WithContext(context.Background()))
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to save user login metadata after updating LID")
|
||||
} else {
|
||||
log.Info().Msg("Updated LID in user login metadata")
|
||||
}
|
||||
}()
|
||||
}
|
||||
}
|
||||
case *events.OfflineSyncPreview:
|
||||
log.Info().
|
||||
Int("message_count", evt.Messages).
|
||||
|
|
@ -179,15 +189,12 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
|
|||
case *events.OfflineSyncCompleted:
|
||||
if !wa.PhoneRecentlySeen(true) {
|
||||
log.Info().
|
||||
Int("evt_count", evt.Count).
|
||||
Time("phone_last_seen", wa.UserLogin.Metadata.(*waid.UserLoginMetadata).PhoneLastSeen.Time).
|
||||
Msg("Offline sync completed, but phone last seen date is still old")
|
||||
Msg("Offline sync completed, but phone last seen date is still old - sending phone offline bridge status")
|
||||
wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateTransientDisconnect, Error: WAPhoneOffline})
|
||||
} else {
|
||||
log.Info().
|
||||
Int("evt_count", evt.Count).
|
||||
Msg("Offline sync completed")
|
||||
log.Info().Msg("Offline sync completed")
|
||||
}
|
||||
wa.UserLogin.BridgeState.Send(status.BridgeState{StateEvent: status.StateConnected})
|
||||
wa.notifyOfflineSyncWaiter(nil)
|
||||
case *events.LoggedOut:
|
||||
wa.handleWALogout(evt.Reason, evt.OnConnect)
|
||||
|
|
@ -243,78 +250,22 @@ func (wa *WhatsAppClient) handleWAEvent(rawEvt any) (success bool) {
|
|||
default:
|
||||
log.Debug().Type("event_type", rawEvt).Msg("Unhandled WhatsApp event")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) rerouteWAMessage(ctx context.Context, evtType string, info *types.MessageSource, msgID any) {
|
||||
if (info.Chat.Server == types.HiddenUserServer || info.Chat.Server == types.BroadcastServer) &&
|
||||
info.Sender.Server == types.HiddenUserServer && info.SenderAlt.IsEmpty() {
|
||||
info.SenderAlt, _ = wa.GetStore().LIDs.GetPNForLID(ctx, info.Sender)
|
||||
}
|
||||
if info.Chat.Server == types.HiddenUserServer && info.IsFromMe && info.RecipientAlt.IsEmpty() {
|
||||
info.RecipientAlt, _ = wa.GetStore().LIDs.GetPNForLID(ctx, info.Chat)
|
||||
}
|
||||
if info.Chat.Server == types.HiddenUserServer && info.Sender.ToNonAD() == info.Chat && info.SenderAlt.Server == types.DefaultUserServer {
|
||||
wa.UserLogin.Log.Debug().
|
||||
Stringer("lid", info.Sender).
|
||||
Stringer("pn", info.SenderAlt).
|
||||
Any("message_id", msgID).
|
||||
Str("evt_type", evtType).
|
||||
Msg("Forced LID DM sender to phone number in incoming message")
|
||||
info.Sender, info.SenderAlt = info.SenderAlt, info.Sender
|
||||
info.Chat = info.Sender.ToNonAD()
|
||||
} else if info.Chat.Server == types.HiddenUserServer && info.IsFromMe && info.RecipientAlt.Server == types.DefaultUserServer {
|
||||
wa.UserLogin.Log.Debug().
|
||||
Stringer("lid", info.Chat).
|
||||
Stringer("pn", info.RecipientAlt).
|
||||
Any("message_id", msgID).
|
||||
Str("evt_type", evtType).
|
||||
Msg("Forced LID DM sender to phone number in own message sent from another device")
|
||||
info.Chat = info.RecipientAlt.ToNonAD()
|
||||
if info.Sender.Server == types.HiddenUserServer {
|
||||
info.Sender, info.SenderAlt = info.SenderAlt, info.Sender
|
||||
if info.Sender.IsEmpty() {
|
||||
info.Sender = wa.GetStore().GetJID()
|
||||
info.Sender.Device = info.SenderAlt.Device
|
||||
}
|
||||
}
|
||||
} else if info.Chat.Server == types.BroadcastServer && info.Sender.Server == types.HiddenUserServer && info.SenderAlt.Server == types.DefaultUserServer {
|
||||
wa.UserLogin.Log.Debug().
|
||||
Stringer("lid", info.Sender).
|
||||
Stringer("pn", info.SenderAlt).
|
||||
Stringer("chat", info.Chat).
|
||||
Any("message_id", msgID).
|
||||
Str("evt_type", evtType).
|
||||
Msg("Forced LID broadcast list sender to phone number in incoming message")
|
||||
info.Sender, info.SenderAlt = info.SenderAlt, info.Sender
|
||||
} else if info.Sender.Server == types.BotServer && info.Chat.Server == types.HiddenUserServer {
|
||||
chatPN, err := wa.GetStore().LIDs.GetPNForLID(ctx, info.Chat)
|
||||
if err != nil {
|
||||
wa.UserLogin.Log.Err(err).
|
||||
Any("message_id", msgID).
|
||||
Stringer("lid", info.Chat).
|
||||
Str("evt_type", evtType).
|
||||
Msg("Failed to get phone number of DM for incoming bot message")
|
||||
} else if !chatPN.IsEmpty() {
|
||||
wa.UserLogin.Log.Debug().
|
||||
Stringer("lid", info.Chat).
|
||||
Stringer("pn", chatPN).
|
||||
Any("message_id", msgID).
|
||||
Str("evt_type", evtType).
|
||||
Msg("Forced LID chat to phone number in bot message")
|
||||
info.Chat = chatPN
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Message) (success bool) {
|
||||
success = true
|
||||
func (wa *WhatsAppClient) handleWAMessage(evt *events.Message) {
|
||||
wa.UserLogin.Log.Trace().
|
||||
Any("info", evt.Info).
|
||||
Any("payload", evt.Message).
|
||||
Msg("Received WhatsApp message")
|
||||
if evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast {
|
||||
return
|
||||
}
|
||||
parsedMessageType := getMessageType(evt.Message)
|
||||
if parsedMessageType == "ignore" || strings.HasPrefix(parsedMessageType, "unknown_protocol_") {
|
||||
return
|
||||
}
|
||||
if encReact := evt.Message.GetEncReactionMessage(); encReact != nil {
|
||||
decrypted, err := wa.Client.DecryptReaction(ctx, evt)
|
||||
decrypted, err := wa.Client.DecryptReaction(evt)
|
||||
if err != nil {
|
||||
wa.UserLogin.Log.Err(err).Str("message_id", evt.Info.ID).Msg("Failed to decrypt reaction")
|
||||
return
|
||||
|
|
@ -323,7 +274,7 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
|
|||
evt.Message.ReactionMessage = decrypted
|
||||
}
|
||||
if encComment := evt.Message.GetEncCommentMessage(); encComment != nil {
|
||||
decrypted, err := wa.Client.DecryptComment(ctx, evt)
|
||||
decrypted, err := wa.Client.DecryptComment(evt)
|
||||
if err != nil {
|
||||
wa.UserLogin.Log.Err(err).Str("message_id", evt.Info.ID).Msg("Failed to decrypt comment")
|
||||
} else {
|
||||
|
|
@ -331,62 +282,7 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
|
|||
evt.Message = decrypted
|
||||
}
|
||||
}
|
||||
if encMessage := evt.Message.GetSecretEncryptedMessage(); encMessage != nil {
|
||||
decrypted, err := wa.Client.DecryptSecretEncryptedMessage(ctx, evt)
|
||||
if err != nil {
|
||||
wa.UserLogin.Log.Err(err).
|
||||
Str("message_id", evt.Info.ID).
|
||||
Stringer("evt_sender", evt.Info.Sender).
|
||||
Any("target_message_key", encMessage.TargetMessageKey).
|
||||
Msg("Failed to decrypt secret-encrypted message")
|
||||
return
|
||||
}
|
||||
evt.RawMessage = decrypted
|
||||
evt.UnwrapRaw()
|
||||
parsedMessageType = getMessageType(evt.Message)
|
||||
}
|
||||
wa.rerouteWAMessage(ctx, "message", &evt.Info.MessageSource, evt.Info.ID)
|
||||
wa.UserLogin.Log.Trace().
|
||||
Any("info", evt.Info).
|
||||
Any("payload", evt.Message).
|
||||
Msg("Received WhatsApp message")
|
||||
if evt.Info.IsFromMe &&
|
||||
evt.Message.GetProtocolMessage().GetHistorySyncNotification() != nil &&
|
||||
wa.Main.Bridge.Config.Backfill.Enabled {
|
||||
wa.saveWAHistorySyncNotification(ctx, evt.Message.ProtocolMessage.HistorySyncNotification)
|
||||
}
|
||||
if parsedMessageType == "ignore" || strings.HasPrefix(parsedMessageType, "unknown_protocol_") {
|
||||
return
|
||||
}
|
||||
|
||||
messageAssoc := evt.Message.GetMessageContextInfo().GetMessageAssociation()
|
||||
if assocType := messageAssoc.GetAssociationType(); assocType == waE2E.MessageAssociation_HD_IMAGE_DUAL_UPLOAD || assocType == waE2E.MessageAssociation_HD_VIDEO_DUAL_UPLOAD {
|
||||
parentKey := messageAssoc.GetParentMessageKey()
|
||||
associatedMessage := evt.Message.GetAssociatedChildMessage().GetMessage()
|
||||
wa.UserLogin.Log.Debug().
|
||||
Str("message_id", evt.Info.ID).
|
||||
Str("parent_id", parentKey.GetID()).
|
||||
Stringer("assoc_type", assocType).
|
||||
Msg("Received HD replacement message, converting to edit")
|
||||
|
||||
protocolMsg := &waE2E.ProtocolMessage{
|
||||
Type: waE2E.ProtocolMessage_MESSAGE_EDIT.Enum(),
|
||||
Key: parentKey,
|
||||
EditedMessage: associatedMessage,
|
||||
}
|
||||
evt.Message = &waE2E.Message{
|
||||
ProtocolMessage: protocolMsg,
|
||||
}
|
||||
} else if assocType == waE2E.MessageAssociation_MOTION_PHOTO {
|
||||
//evt.Message = evt.Message.GetAssociatedChildMessage().GetMessage()
|
||||
wa.UserLogin.Log.Debug().
|
||||
Str("message_id", evt.Info.ID).
|
||||
Str("parent_id", messageAssoc.GetParentMessageKey().GetID()).
|
||||
Msg("Ignoring motion photo update")
|
||||
return
|
||||
}
|
||||
|
||||
res := wa.UserLogin.QueueRemoteEvent(&WAMessageEvent{
|
||||
wa.Main.Bridge.QueueRemoteEvent(wa.UserLogin, &WAMessageEvent{
|
||||
MessageInfoWrapper: &MessageInfoWrapper{
|
||||
Info: evt.Info,
|
||||
wa: wa,
|
||||
|
|
@ -396,11 +292,9 @@ func (wa *WhatsAppClient) handleWAMessage(ctx context.Context, evt *events.Messa
|
|||
|
||||
parsedMessageType: parsedMessageType,
|
||||
})
|
||||
return res.Success
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWAUndecryptableMessage(ctx context.Context, evt *events.UndecryptableMessage) bool {
|
||||
wa.rerouteWAMessage(ctx, "undecryptable message", &evt.Info.MessageSource, evt.Info.ID)
|
||||
func (wa *WhatsAppClient) handleWAUndecryptableMessage(evt *events.UndecryptableMessage) {
|
||||
wa.UserLogin.Log.Debug().
|
||||
Any("info", evt.Info).
|
||||
Bool("unavailable", evt.IsUnavailable).
|
||||
|
|
@ -408,24 +302,21 @@ func (wa *WhatsAppClient) handleWAUndecryptableMessage(ctx context.Context, evt
|
|||
Msg("Received undecryptable WhatsApp message")
|
||||
wa.trackUndecryptable(evt)
|
||||
if evt.DecryptFailMode == events.DecryptFailHide {
|
||||
return true
|
||||
return
|
||||
}
|
||||
if evt.Info.Chat == types.StatusBroadcastJID && !wa.Main.Config.EnableStatusBroadcast {
|
||||
return true
|
||||
return
|
||||
}
|
||||
res := wa.UserLogin.QueueRemoteEvent(&WAUndecryptableMessage{
|
||||
wa.Main.Bridge.QueueRemoteEvent(wa.UserLogin, &WAUndecryptableMessage{
|
||||
MessageInfoWrapper: &MessageInfoWrapper{
|
||||
Info: evt.Info,
|
||||
wa: wa,
|
||||
},
|
||||
Type: evt.UnavailableType,
|
||||
})
|
||||
return res.Success
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Receipt) (success bool) {
|
||||
origChat := evt.Chat
|
||||
wa.rerouteWAMessage(ctx, "receipt", &evt.MessageSource, evt.MessageIDs)
|
||||
func (wa *WhatsAppClient) handleWAReceipt(evt *events.Receipt) {
|
||||
if evt.IsFromMe && evt.Sender.Device == 0 {
|
||||
wa.phoneSeen(evt.Timestamp)
|
||||
}
|
||||
|
|
@ -438,47 +329,28 @@ func (wa *WhatsAppClient) handleWAReceipt(ctx context.Context, evt *events.Recei
|
|||
case types.ReceiptTypeSender:
|
||||
fallthrough
|
||||
default:
|
||||
return true
|
||||
return
|
||||
}
|
||||
targets := make([]networkid.MessageID, len(evt.MessageIDs))
|
||||
messageSender := wa.JID
|
||||
if !evt.MessageSender.IsEmpty() {
|
||||
messageSender = evt.MessageSender
|
||||
// Second part of rerouting receipts in LID chats
|
||||
if messageSender == origChat && evt.Chat != origChat {
|
||||
messageSender = evt.Chat
|
||||
}
|
||||
} else if evt.Chat.Server == types.GroupServer && evt.Sender.Server == types.HiddenUserServer {
|
||||
lid := wa.GetStore().GetLID()
|
||||
if !lid.IsEmpty() {
|
||||
messageSender = lid
|
||||
}
|
||||
}
|
||||
for i, id := range evt.MessageIDs {
|
||||
targets[i] = waid.MakeMessageID(evt.Chat, messageSender, id)
|
||||
}
|
||||
res := wa.UserLogin.QueueRemoteEvent(&simplevent.Receipt{
|
||||
wa.Main.Bridge.QueueRemoteEvent(wa.UserLogin, &simplevent.Receipt{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: evtType,
|
||||
PortalKey: wa.makeWAPortalKey(evt.Chat),
|
||||
Sender: wa.makeEventSender(ctx, evt.Sender),
|
||||
Sender: wa.makeEventSender(evt.Sender),
|
||||
Timestamp: evt.Timestamp,
|
||||
},
|
||||
Targets: targets,
|
||||
})
|
||||
return res.Success
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWAChatPresence(ctx context.Context, evt *events.ChatPresence) {
|
||||
if evt.Chat.Server == types.HiddenUserServer && evt.Sender.ToNonAD() == evt.Chat {
|
||||
if evt.SenderAlt.IsEmpty() {
|
||||
evt.SenderAlt, _ = wa.GetStore().LIDs.GetPNForLID(ctx, evt.Sender)
|
||||
}
|
||||
if evt.SenderAlt.Server == types.DefaultUserServer {
|
||||
evt.Sender, evt.SenderAlt = evt.SenderAlt, evt.Sender
|
||||
evt.Chat = evt.Sender.ToNonAD()
|
||||
}
|
||||
}
|
||||
func (wa *WhatsAppClient) handleWAChatPresence(evt *events.ChatPresence) {
|
||||
typingType := bridgev2.TypingTypeText
|
||||
timeout := 15 * time.Second
|
||||
if evt.Media == types.ChatPresenceMediaAudio {
|
||||
|
|
@ -488,12 +360,12 @@ func (wa *WhatsAppClient) handleWAChatPresence(ctx context.Context, evt *events.
|
|||
timeout = 0
|
||||
}
|
||||
|
||||
wa.UserLogin.QueueRemoteEvent(&simplevent.Typing{
|
||||
wa.Main.Bridge.QueueRemoteEvent(wa.UserLogin, &simplevent.Typing{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventTyping,
|
||||
LogContext: nil,
|
||||
PortalKey: wa.makeWAPortalKey(evt.Chat),
|
||||
Sender: wa.makeEventSender(ctx, evt.Sender),
|
||||
Sender: wa.makeEventSender(evt.Sender),
|
||||
Timestamp: time.Now(),
|
||||
},
|
||||
Timeout: timeout,
|
||||
|
|
@ -508,7 +380,7 @@ func (wa *WhatsAppClient) handleWALogout(reason events.ConnectFailureReason, onC
|
|||
} else if reason == events.ConnectFailureMainDeviceGone {
|
||||
errorCode = WAMainDeviceGone
|
||||
}
|
||||
wa.Disconnect()
|
||||
wa.Client.Disconnect()
|
||||
wa.Client = nil
|
||||
wa.JID = types.EmptyJID
|
||||
wa.UserLogin.Metadata.(*waid.UserLoginMetadata).WADeviceID = 0
|
||||
|
|
@ -520,36 +392,23 @@ func (wa *WhatsAppClient) handleWALogout(reason events.ConnectFailureReason, onC
|
|||
|
||||
const callEventMaxAge = 15 * time.Minute
|
||||
|
||||
func (wa *WhatsAppClient) handleWACallStart(ctx context.Context, group, sender, senderAlt types.JID, id, callType string, ts time.Time) bool {
|
||||
func (wa *WhatsAppClient) handleWACallStart(sender types.JID, id, callType string, ts time.Time) {
|
||||
if !wa.Main.Config.CallStartNotices || time.Since(ts) > callEventMaxAge {
|
||||
return true
|
||||
return
|
||||
}
|
||||
if sender.Server == types.HiddenUserServer && senderAlt.Server == types.DefaultUserServer {
|
||||
wa.UserLogin.Log.Debug().
|
||||
Stringer("lid", sender).
|
||||
Stringer("pn", senderAlt).
|
||||
Str("call_id", id).
|
||||
Msg("Forced LID caller to phone number in incoming call")
|
||||
sender, senderAlt = senderAlt, sender
|
||||
}
|
||||
chat := group
|
||||
if chat.IsEmpty() {
|
||||
chat = sender
|
||||
}
|
||||
return wa.UserLogin.QueueRemoteEvent(&simplevent.Message[string]{
|
||||
wa.UserLogin.QueueRemoteEvent(&simplevent.Message[string]{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventMessage,
|
||||
LogContext: nil,
|
||||
PortalKey: wa.makeWAPortalKey(chat),
|
||||
Sender: wa.makeEventSender(ctx, sender),
|
||||
PortalKey: wa.makeWAPortalKey(sender),
|
||||
Sender: wa.makeEventSender(sender),
|
||||
CreatePortal: true,
|
||||
Timestamp: ts,
|
||||
StreamOrder: ts.Unix(),
|
||||
},
|
||||
Data: callType,
|
||||
ID: waid.MakeFakeMessageID(chat, sender, "call-"+id),
|
||||
ID: waid.MakeFakeMessageID(sender, sender, "call-"+id),
|
||||
ConvertMessageFunc: convertCallStart,
|
||||
}).Success
|
||||
})
|
||||
}
|
||||
|
||||
func convertCallStart(ctx context.Context, portal *bridgev2.Portal, intent bridgev2.MatrixAPI, callType string) (*bridgev2.ConvertedMessage, error) {
|
||||
|
|
@ -563,16 +422,12 @@ func convertCallStart(ctx context.Context, portal *bridgev2.Portal, intent bridg
|
|||
Content: &event.MessageEventContent{
|
||||
MsgType: event.MsgText,
|
||||
Body: text,
|
||||
BeeperActionMessage: &event.BeeperActionMessage{
|
||||
Type: event.BeeperActionMessageCall,
|
||||
CallType: event.BeeperActionMessageCallType(callType),
|
||||
},
|
||||
},
|
||||
}},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWAIdentityChange(ctx context.Context, evt *events.IdentityChange) {
|
||||
func (wa *WhatsAppClient) handleWAIdentityChange(evt *events.IdentityChange) {
|
||||
if !wa.Main.Config.IdentityChangeNotices {
|
||||
return
|
||||
}
|
||||
|
|
@ -581,7 +436,7 @@ func (wa *WhatsAppClient) handleWAIdentityChange(ctx context.Context, evt *event
|
|||
Type: bridgev2.RemoteEventMessage,
|
||||
LogContext: nil,
|
||||
PortalKey: wa.makeWAPortalKey(evt.JID),
|
||||
Sender: wa.makeEventSender(ctx, evt.JID),
|
||||
Sender: wa.makeEventSender(evt.JID),
|
||||
CreatePortal: false,
|
||||
Timestamp: evt.Timestamp,
|
||||
},
|
||||
|
|
@ -611,43 +466,39 @@ func convertIdentityChange(ctx context.Context, portal *bridgev2.Portal, intent
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWADeleteChat(ctx context.Context, evt *events.DeleteChat) bool {
|
||||
chatJID := wa.maybeConvertJIDToLID(ctx, evt.JID)
|
||||
return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatDelete{
|
||||
func (wa *WhatsAppClient) handleWADeleteChat(evt *events.DeleteChat) {
|
||||
wa.UserLogin.QueueRemoteEvent(&simplevent.ChatDelete{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventChatDelete,
|
||||
PortalKey: wa.makeWAPortalKey(chatJID),
|
||||
PortalKey: wa.makeWAPortalKey(evt.JID),
|
||||
Timestamp: evt.Timestamp,
|
||||
},
|
||||
OnlyForMe: true,
|
||||
Children: true,
|
||||
}).Success
|
||||
})
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWADeleteForMe(ctx context.Context, evt *events.DeleteForMe) bool {
|
||||
chatJID := wa.maybeConvertJIDToLID(ctx, evt.ChatJID)
|
||||
return wa.UserLogin.QueueRemoteEvent(&simplevent.MessageRemove{
|
||||
func (wa *WhatsAppClient) handleWADeleteForMe(evt *events.DeleteForMe) {
|
||||
wa.UserLogin.QueueRemoteEvent(&simplevent.MessageRemove{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventMessageRemove,
|
||||
PortalKey: wa.makeWAPortalKey(chatJID),
|
||||
PortalKey: wa.makeWAPortalKey(evt.ChatJID),
|
||||
Timestamp: evt.Timestamp,
|
||||
},
|
||||
TargetMessage: waid.MakeMessageID(chatJID, evt.SenderJID, evt.MessageID),
|
||||
TargetMessage: waid.MakeMessageID(evt.ChatJID, evt.SenderJID, evt.MessageID),
|
||||
OnlyForMe: true,
|
||||
}).Success
|
||||
})
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWAMarkChatAsRead(ctx context.Context, evt *events.MarkChatAsRead) bool {
|
||||
chatJID := wa.maybeConvertJIDToLID(ctx, evt.JID)
|
||||
return wa.UserLogin.QueueRemoteEvent(&simplevent.Receipt{
|
||||
func (wa *WhatsAppClient) handleWAMarkChatAsRead(evt *events.MarkChatAsRead) {
|
||||
wa.UserLogin.QueueRemoteEvent(&simplevent.Receipt{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventReadReceipt,
|
||||
PortalKey: wa.makeWAPortalKey(chatJID),
|
||||
Sender: wa.makeEventSender(ctx, wa.JID),
|
||||
PortalKey: wa.makeWAPortalKey(evt.JID),
|
||||
Sender: wa.makeEventSender(wa.JID),
|
||||
Timestamp: evt.Timestamp,
|
||||
},
|
||||
ReadUpTo: evt.Timestamp,
|
||||
}).Success
|
||||
})
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) syncGhost(jid types.JID, reason string, pictureID *string) {
|
||||
|
|
@ -657,7 +508,7 @@ func (wa *WhatsAppClient) syncGhost(jid types.JID, reason string, pictureID *str
|
|||
Str("picture_id", ptr.Val(pictureID)).
|
||||
Stringer("jid", jid).
|
||||
Logger()
|
||||
ctx := log.WithContext(wa.Main.Bridge.BackgroundCtx)
|
||||
ctx := log.WithContext(context.Background())
|
||||
ghost, err := wa.Main.Bridge.GetGhostByID(ctx, waid.MakeUserID(jid))
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to get ghost")
|
||||
|
|
@ -672,15 +523,12 @@ func (wa *WhatsAppClient) syncGhost(jid types.JID, reason string, pictureID *str
|
|||
} else {
|
||||
ghost.UpdateInfo(ctx, userInfo)
|
||||
log.Debug().Msg("Synced ghost info")
|
||||
wa.syncAltGhostWithInfo(ctx, jid, userInfo)
|
||||
}
|
||||
go wa.syncRemoteProfile(ctx, ghost)
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWAPictureUpdate(ctx context.Context, evt *events.Picture) bool {
|
||||
if evt.JID.Server == types.DefaultUserServer || evt.JID.Server == types.HiddenUserServer || evt.JID.Server == types.BotServer {
|
||||
go wa.syncGhost(evt.JID, "picture event", &evt.PictureID)
|
||||
return true
|
||||
func (wa *WhatsAppClient) handleWAPictureUpdate(evt *events.Picture) {
|
||||
if evt.JID.Server == types.DefaultUserServer || evt.JID.Server == types.BotServer {
|
||||
wa.syncGhost(evt.JID, "picture event", &evt.PictureID)
|
||||
} else {
|
||||
var changes bridgev2.ChatInfo
|
||||
if evt.Remove {
|
||||
|
|
@ -688,7 +536,7 @@ func (wa *WhatsAppClient) handleWAPictureUpdate(ctx context.Context, evt *events
|
|||
} else {
|
||||
changes.ExtraUpdates = wa.makePortalAvatarFetcher(evt.PictureID, evt.Author, evt.Timestamp)
|
||||
}
|
||||
return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatInfoChange{
|
||||
wa.UserLogin.QueueRemoteEvent(&simplevent.ChatInfoChange{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventChatInfoChange,
|
||||
LogContext: func(c zerolog.Context) zerolog.Context {
|
||||
|
|
@ -699,17 +547,17 @@ func (wa *WhatsAppClient) handleWAPictureUpdate(ctx context.Context, evt *events
|
|||
Bool("remove_picture", evt.Remove)
|
||||
},
|
||||
PortalKey: wa.makeWAPortalKey(evt.JID),
|
||||
Sender: wa.makeEventSender(ctx, evt.Author),
|
||||
Sender: wa.makeEventSender(evt.Author),
|
||||
Timestamp: evt.Timestamp,
|
||||
},
|
||||
ChatInfoChange: &bridgev2.ChatInfoChange{
|
||||
ChatInfo: &changes,
|
||||
},
|
||||
}).Success
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWAGroupInfoChange(ctx context.Context, evt *events.GroupInfo) bool {
|
||||
func (wa *WhatsAppClient) handleWAGroupInfoChange(evt *events.GroupInfo) {
|
||||
eventMeta := simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventChatInfoChange,
|
||||
LogContext: nil,
|
||||
|
|
@ -718,59 +566,56 @@ func (wa *WhatsAppClient) handleWAGroupInfoChange(ctx context.Context, evt *even
|
|||
Timestamp: evt.Timestamp,
|
||||
}
|
||||
if evt.Sender != nil {
|
||||
eventMeta.Sender = wa.makeEventSender(ctx, *evt.Sender)
|
||||
eventMeta.Sender = wa.makeEventSender(*evt.Sender)
|
||||
}
|
||||
if evt.Delete != nil {
|
||||
eventMeta.Type = bridgev2.RemoteEventChatDelete
|
||||
return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatDelete{EventMeta: eventMeta}).Success
|
||||
wa.UserLogin.QueueRemoteEvent(&simplevent.ChatDelete{EventMeta: eventMeta})
|
||||
} else {
|
||||
return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatInfoChange{
|
||||
wa.UserLogin.QueueRemoteEvent(&simplevent.ChatInfoChange{
|
||||
EventMeta: eventMeta,
|
||||
ChatInfoChange: wa.wrapGroupInfoChange(ctx, evt),
|
||||
}).Success
|
||||
ChatInfoChange: wa.wrapGroupInfoChange(evt),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWAJoinedGroup(ctx context.Context, evt *events.JoinedGroup) bool {
|
||||
if wa.createDedup.Pop(evt.CreateKey) {
|
||||
return true
|
||||
}
|
||||
return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{
|
||||
func (wa *WhatsAppClient) handleWAJoinedGroup(evt *events.JoinedGroup) {
|
||||
wa.Main.Bridge.QueueRemoteEvent(wa.UserLogin, &simplevent.ChatResync{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventChatResync,
|
||||
LogContext: nil,
|
||||
PortalKey: wa.makeWAPortalKey(evt.JID),
|
||||
CreatePortal: true,
|
||||
},
|
||||
ChatInfo: wa.wrapGroupInfo(ctx, &evt.GroupInfo),
|
||||
}).Success
|
||||
ChatInfo: wa.wrapGroupInfo(&evt.GroupInfo),
|
||||
})
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWANewsletterJoin(ctx context.Context, evt *events.NewsletterJoin) bool {
|
||||
return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{
|
||||
func (wa *WhatsAppClient) handleWANewsletterJoin(evt *events.NewsletterJoin) {
|
||||
wa.Main.Bridge.QueueRemoteEvent(wa.UserLogin, &simplevent.ChatResync{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventChatResync,
|
||||
LogContext: nil,
|
||||
PortalKey: wa.makeWAPortalKey(evt.ID),
|
||||
CreatePortal: true,
|
||||
},
|
||||
ChatInfo: wa.wrapNewsletterInfo(ctx, &evt.NewsletterMetadata),
|
||||
}).Success
|
||||
ChatInfo: wa.wrapNewsletterInfo(&evt.NewsletterMetadata),
|
||||
})
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWANewsletterLeave(evt *events.NewsletterLeave) bool {
|
||||
return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatDelete{
|
||||
func (wa *WhatsAppClient) handleWANewsletterLeave(evt *events.NewsletterLeave) {
|
||||
wa.Main.Bridge.QueueRemoteEvent(wa.UserLogin, &simplevent.ChatDelete{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventChatDelete,
|
||||
LogContext: nil,
|
||||
PortalKey: wa.makeWAPortalKey(evt.ID),
|
||||
},
|
||||
OnlyForMe: true,
|
||||
}).Success
|
||||
})
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWAUserLocalPortalInfo(chatJID types.JID, ts time.Time, info *bridgev2.UserLocalPortalInfo) bool {
|
||||
return wa.UserLogin.QueueRemoteEvent(&simplevent.ChatInfoChange{
|
||||
func (wa *WhatsAppClient) handleWAUserLocalPortalInfo(chatJID types.JID, ts time.Time, info *bridgev2.UserLocalPortalInfo) {
|
||||
wa.UserLogin.QueueRemoteEvent(&simplevent.ChatInfoChange{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventChatInfoChange,
|
||||
PortalKey: wa.makeWAPortalKey(chatJID),
|
||||
|
|
@ -781,136 +626,40 @@ func (wa *WhatsAppClient) handleWAUserLocalPortalInfo(chatJID types.JID, ts time
|
|||
UserLocal: info,
|
||||
},
|
||||
},
|
||||
}).Success
|
||||
})
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWAMute(evt *events.Mute) bool {
|
||||
func (wa *WhatsAppClient) handleWAMute(evt *events.Mute) {
|
||||
var mutedUntil time.Time
|
||||
if evt.Action.GetMuted() {
|
||||
mutedUntil = event.MutedForever
|
||||
if evt.Action.GetMuteEndTimestamp() > 0 {
|
||||
if evt.Action.GetMuteEndTimestamp() != 0 {
|
||||
mutedUntil = time.Unix(evt.Action.GetMuteEndTimestamp(), 0)
|
||||
}
|
||||
} else {
|
||||
mutedUntil = bridgev2.Unmuted
|
||||
}
|
||||
return wa.handleWAUserLocalPortalInfo(evt.JID, evt.Timestamp, &bridgev2.UserLocalPortalInfo{
|
||||
wa.handleWAUserLocalPortalInfo(evt.JID, evt.Timestamp, &bridgev2.UserLocalPortalInfo{
|
||||
MutedUntil: &mutedUntil,
|
||||
})
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWAArchive(evt *events.Archive) bool {
|
||||
func (wa *WhatsAppClient) handleWAArchive(evt *events.Archive) {
|
||||
var tag event.RoomTag
|
||||
if evt.Action.GetArchived() {
|
||||
tag = wa.Main.Config.ArchiveTag
|
||||
}
|
||||
return wa.handleWAUserLocalPortalInfo(evt.JID, evt.Timestamp, &bridgev2.UserLocalPortalInfo{
|
||||
wa.handleWAUserLocalPortalInfo(evt.JID, evt.Timestamp, &bridgev2.UserLocalPortalInfo{
|
||||
Tag: &tag,
|
||||
})
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWAPin(evt *events.Pin) bool {
|
||||
func (wa *WhatsAppClient) handleWAPin(evt *events.Pin) {
|
||||
var tag event.RoomTag
|
||||
if evt.Action.GetPinned() {
|
||||
tag = wa.Main.Config.PinnedTag
|
||||
}
|
||||
return wa.handleWAUserLocalPortalInfo(evt.JID, evt.Timestamp, &bridgev2.UserLocalPortalInfo{
|
||||
wa.handleWAUserLocalPortalInfo(evt.JID, evt.Timestamp, &bridgev2.UserLocalPortalInfo{
|
||||
Tag: &tag,
|
||||
})
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWAAppStateSyncComplete(ctx context.Context, evt *events.AppStateSyncComplete) {
|
||||
log := zerolog.Ctx(ctx).With().
|
||||
Str("patch_name", string(evt.Name)).
|
||||
Uint64("patch_version", evt.Version).
|
||||
Logger()
|
||||
if len(wa.GetStore().PushName) > 0 && evt.Name == appstate.WAPatchCriticalBlock {
|
||||
err := wa.updatePresence(ctx, types.PresenceUnavailable)
|
||||
if err != nil {
|
||||
log.Warn().Err(err).Msg("Failed to send presence after app state sync")
|
||||
}
|
||||
go wa.syncRemoteProfile(log.WithContext(context.Background()), nil)
|
||||
} else if evt.Name == appstate.WAPatchCriticalUnblockLow {
|
||||
go wa.resyncContacts(false, true)
|
||||
}
|
||||
wa.appStateRecoveryLock.Lock()
|
||||
defer wa.appStateRecoveryLock.Unlock()
|
||||
meta := wa.UserLogin.Metadata.(*waid.UserLoginMetadata)
|
||||
if ts, exists := meta.AppStateRecoveryAttempted[evt.Name]; exists {
|
||||
delete(wa.appStateFullSyncAttempted, evt.Name)
|
||||
delete(meta.AppStateRecoveryAttempted, evt.Name)
|
||||
err := wa.UserLogin.Save(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to save login metadata after unmarking app state recovery as attempted")
|
||||
} else {
|
||||
log.Info().
|
||||
Time("recovery_ts", ts).
|
||||
Msg("Unmarked app state recovery as attempted after successful full sync")
|
||||
}
|
||||
} else if ts, exists = wa.appStateFullSyncAttempted[evt.Name]; exists {
|
||||
delete(wa.appStateFullSyncAttempted, evt.Name)
|
||||
log.Debug().Time("full_sync_ts", ts).Msg("Unmarked app state full sync attempted after successful sync")
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) handleWAAppStateSyncError(ctx context.Context, evt *events.AppStateSyncError) {
|
||||
log := zerolog.Ctx(ctx).With().
|
||||
Str("patch_name", string(evt.Name)).
|
||||
Logger()
|
||||
wa.appStateRecoveryLock.Lock()
|
||||
defer wa.appStateRecoveryLock.Unlock()
|
||||
meta := wa.UserLogin.Metadata.(*waid.UserLoginMetadata)
|
||||
lastRecovery := meta.AppStateRecoveryAttempted[evt.Name]
|
||||
lastFullSync := wa.appStateFullSyncAttempted[evt.Name]
|
||||
if !lastRecovery.IsZero() && time.Since(lastRecovery) < 48*time.Hour {
|
||||
log.Debug().Err(evt.Error).
|
||||
Time("last_recovery_attempt", lastRecovery).
|
||||
Time("last_full_sync_attempt", lastFullSync).
|
||||
Msg("App state sync failed, but recovery already attempted")
|
||||
return
|
||||
}
|
||||
if !evt.FullSync {
|
||||
if !lastFullSync.IsZero() {
|
||||
log.Debug().
|
||||
Err(evt.Error).
|
||||
Time("last_full_sync_attempt", lastFullSync).
|
||||
Msg("App state sync failed, but full sync already attempted")
|
||||
return
|
||||
}
|
||||
wa.appStateFullSyncAttempted[evt.Name] = time.Now()
|
||||
log.Info().
|
||||
Err(evt.Error).
|
||||
Msg("Trying full sync for app state after partial sync error")
|
||||
go func() {
|
||||
err := wa.Client.FetchAppState(ctx, evt.Name, true, false)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Full app state sync failed")
|
||||
} else {
|
||||
log.Debug().Msg("Full app state sync succeeded")
|
||||
}
|
||||
}()
|
||||
return
|
||||
}
|
||||
log.Info().
|
||||
Err(evt.Error).
|
||||
Msg("Trying recovery for app state after full sync error")
|
||||
if meta.AppStateRecoveryAttempted == nil {
|
||||
meta.AppStateRecoveryAttempted = make(map[appstate.WAPatchName]time.Time)
|
||||
}
|
||||
meta.AppStateRecoveryAttempted[evt.Name] = time.Now()
|
||||
err := wa.UserLogin.Save(ctx)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to save login metadata after marking app state recovery as attempted")
|
||||
}
|
||||
go func() {
|
||||
resp, err := wa.Client.SendPeerMessage(ctx, whatsmeow.BuildAppStateRecoveryRequest(evt.Name))
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to send app state recovery request")
|
||||
} else {
|
||||
log.Debug().
|
||||
Str("message_id", resp.ID).
|
||||
Time("message_ts", resp.Timestamp).
|
||||
Msg("Sent app state recovery request")
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,9 +1,6 @@
|
|||
package connector
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/ptr"
|
||||
"go.mau.fi/whatsmeow/proto/waCommon"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
|
|
@ -28,30 +25,15 @@ func (wa *WhatsAppClient) makeWAPortalKey(chatJID types.JID) networkid.PortalKey
|
|||
return key
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) makeEventSender(ctx context.Context, id types.JID) bridgev2.EventSender {
|
||||
func (wa *WhatsAppClient) makeEventSender(id types.JID) bridgev2.EventSender {
|
||||
if id.Server == types.NewsletterServer {
|
||||
// Send as bot
|
||||
return bridgev2.EventSender{}
|
||||
}
|
||||
var senderLoginJID types.JID
|
||||
if wa.Main.Bridge.Config.SplitPortals {
|
||||
// no need for sender login ID
|
||||
} else if id.Server == types.DefaultUserServer {
|
||||
senderLoginJID = id
|
||||
} else if id.Server == types.HiddenUserServer {
|
||||
pn, err := wa.GetStore().LIDs.GetPNForLID(ctx, id)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).
|
||||
Stringer("lid", id).
|
||||
Msg("Failed to get phone number for LID to make event sender")
|
||||
} else if !pn.IsEmpty() {
|
||||
senderLoginJID = pn
|
||||
}
|
||||
}
|
||||
return bridgev2.EventSender{
|
||||
IsFromMe: id.User == wa.GetStore().GetJID().User || id.User == wa.GetStore().GetLID().User,
|
||||
IsFromMe: id.User == wa.Client.Store.GetJID().User || id.User == wa.Client.Store.GetLID().User,
|
||||
Sender: waid.MakeUserID(id),
|
||||
SenderLogin: waid.MakeUserLoginID(senderLoginJID),
|
||||
SenderLogin: waid.MakeUserLoginID(id),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -60,7 +42,7 @@ func (wa *WhatsAppClient) messageIDToKey(id *waid.ParsedMessageID) *waCommon.Mes
|
|||
RemoteJID: ptr.Ptr(id.Chat.String()),
|
||||
ID: ptr.Ptr(id.ID),
|
||||
}
|
||||
if id.Sender.User == wa.GetStore().GetJID().User || id.Sender.User == wa.GetStore().GetLID().User {
|
||||
if id.Sender.User == wa.Client.Store.GetJID().User || id.Sender.User == wa.Client.Store.GetLID().User {
|
||||
key.FromMe = ptr.Ptr(true)
|
||||
}
|
||||
if id.Chat.Server != types.MessengerServer && id.Chat.Server != types.DefaultUserServer && id.Chat.Server != types.HiddenUserServer && id.Chat.Server != types.BotServer {
|
||||
|
|
@ -68,16 +50,3 @@ func (wa *WhatsAppClient) messageIDToKey(id *waid.ParsedMessageID) *waCommon.Mes
|
|||
}
|
||||
return key
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) maybeConvertJIDToLID(ctx context.Context, chatJID types.JID) types.JID {
|
||||
if chatJID.Server == types.HiddenUserServer {
|
||||
if pn, err := wa.GetStore().LIDs.GetPNForLID(ctx, chatJID); err != nil {
|
||||
wa.UserLogin.Log.Err(err).
|
||||
Stringer("lid", chatJID).
|
||||
Msg("Failed to get phone number for LID chat")
|
||||
} else if !pn.IsEmpty() {
|
||||
return pn.ToNonAD()
|
||||
}
|
||||
}
|
||||
return chatJID
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@ package connector
|
|||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"sync/atomic"
|
||||
|
|
@ -10,7 +9,6 @@ import (
|
|||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/exsync"
|
||||
"go.mau.fi/util/jsontime"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/types/events"
|
||||
waLog "go.mau.fi/whatsmeow/util/log"
|
||||
|
|
@ -64,21 +62,6 @@ var (
|
|||
Err: "Unexpected event while waiting for login",
|
||||
StatusCode: http.StatusInternalServerError,
|
||||
}
|
||||
ErrPhoneNumberTooShort = bridgev2.RespError{
|
||||
ErrCode: "FI.MAU.WHATSAPP.PHONE_NUMBER_TOO_SHORT",
|
||||
Err: "Phone number too short",
|
||||
StatusCode: http.StatusBadRequest,
|
||||
}
|
||||
ErrPhoneNumberIsNotInternational = bridgev2.RespError{
|
||||
ErrCode: "FI.MAU.WHATSAPP.PHONE_NUMBER_NOT_INTERNATIONAL",
|
||||
Err: "Phone number must be in international format",
|
||||
StatusCode: http.StatusBadRequest,
|
||||
}
|
||||
ErrRateLimitedByWhatsApp = bridgev2.RespError{
|
||||
ErrCode: "FI.MAU.WHATSAPP.RATE_LIMITED",
|
||||
Err: "Rate limited by WhatsApp",
|
||||
StatusCode: http.StatusTooManyRequests,
|
||||
}
|
||||
)
|
||||
|
||||
func (wa *WhatsAppConnector) CreateLogin(_ context.Context, user *bridgev2.User, flowID string) (bridgev2.LoginProcess, error) {
|
||||
|
|
@ -121,7 +104,6 @@ type WALogin struct {
|
|||
var (
|
||||
_ bridgev2.LoginProcessDisplayAndWait = (*WALogin)(nil)
|
||||
_ bridgev2.LoginProcessUserInput = (*WALogin)(nil)
|
||||
_ bridgev2.LoginProcessWithOverride = (*WALogin)(nil)
|
||||
)
|
||||
|
||||
const LoginConnectWait = 15 * time.Second
|
||||
|
|
@ -167,20 +149,6 @@ func (wl *WALogin) Start(ctx context.Context) (*bridgev2.LoginStep, error) {
|
|||
return makeQRStep(wl.QRs[0]), nil
|
||||
}
|
||||
|
||||
func (wl *WALogin) StartWithOverride(ctx context.Context, old *bridgev2.UserLogin) (*bridgev2.LoginStep, error) {
|
||||
step, err := wl.Start(ctx)
|
||||
if err == nil && step != nil && old != nil && step.StepID == LoginStepIDPhoneNumber {
|
||||
phoneNumber := fmt.Sprintf("+%s", old.ID)
|
||||
wl.Log.Debug().
|
||||
Str("phone_number", phoneNumber).
|
||||
Msg("Auto-submitting phone number for relogin")
|
||||
return wl.SubmitUserInput(ctx, map[string]string{
|
||||
"phone_number": phoneNumber,
|
||||
})
|
||||
}
|
||||
return step, err
|
||||
}
|
||||
|
||||
func (wl *WALogin) SubmitUserInput(ctx context.Context, input map[string]string) (*bridgev2.LoginStep, error) {
|
||||
ctx, cancel := context.WithTimeout(ctx, LoginConnectWait)
|
||||
defer cancel()
|
||||
|
|
@ -194,16 +162,9 @@ func (wl *WALogin) SubmitUserInput(ctx context.Context, input map[string]string)
|
|||
wl.Log.Warn().Err(err).Msg("Timed out waiting for connection")
|
||||
return nil, fmt.Errorf("failed to wait for connection: %w", err)
|
||||
}
|
||||
pairingCode, err := wl.Client.PairPhone(ctx, input["phone_number"], true, whatsmeow.PairClientChrome, "Chrome (Linux)")
|
||||
pairingCode, err := wl.Client.PairPhone(input["phone_number"], true, whatsmeow.PairClientChrome, "Chrome (Linux)")
|
||||
if err != nil {
|
||||
wl.Log.Err(err).Msg("Failed to request phone code login")
|
||||
if errors.Is(err, whatsmeow.ErrPhoneNumberTooShort) {
|
||||
return nil, ErrPhoneNumberTooShort
|
||||
} else if errors.Is(err, whatsmeow.ErrPhoneNumberIsNotInternational) {
|
||||
return nil, ErrPhoneNumberIsNotInternational
|
||||
} else if errors.Is(err, whatsmeow.ErrIQRateOverLimit) {
|
||||
return nil, ErrRateLimitedByWhatsApp
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
wl.Log.Debug().Msg("Phone code login started")
|
||||
|
|
@ -345,8 +306,8 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
|
|||
Name: wl.LoginSuccess.BusinessName,
|
||||
},
|
||||
Metadata: &waid.UserLoginMetadata{
|
||||
WALID: wl.LoginSuccess.LID.User,
|
||||
WADeviceID: wl.LoginSuccess.ID.Device,
|
||||
LoggedInAt: jsontime.UnixNow(),
|
||||
Timezone: wl.Timezone,
|
||||
|
||||
HistorySyncPortalsNeedCreating: true,
|
||||
|
|
@ -359,7 +320,16 @@ func (wl *WALogin) Wait(ctx context.Context) (*bridgev2.LoginStep, error) {
|
|||
}
|
||||
|
||||
ul.Client.(*WhatsAppClient).isNewLogin = true
|
||||
ul.Client.Connect(ul.Log.WithContext(wl.Main.Bridge.BackgroundCtx))
|
||||
ul.Client.Connect(ul.Log.WithContext(context.Background()))
|
||||
|
||||
if contact, err := ul.Client.(*WhatsAppClient).GetStore().Contacts.GetContact(wl.LoginSuccess.ID); err != nil {
|
||||
wl.Log.Err(err).Msg("Failed to get own contact after login")
|
||||
} else {
|
||||
contactInfo := ul.Client.(*WhatsAppClient).contactToUserInfo(ctx, wl.LoginSuccess.ID, contact, true)
|
||||
if contactInfo.Name != nil {
|
||||
ul.UserLogin.RemoteProfile.Username = *contactInfo.Name
|
||||
}
|
||||
}
|
||||
|
||||
return &bridgev2.LoginStep{
|
||||
Type: bridgev2.LoginStepTypeComplete,
|
||||
|
|
|
|||
|
|
@ -1,81 +0,0 @@
|
|||
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
|
||||
// Copyright (C) 2026 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package connector
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"go.mau.fi/whatsmeow"
|
||||
waBinary "go.mau.fi/whatsmeow/binary"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
|
||||
"go.mau.fi/mautrix-whatsapp/pkg/waid"
|
||||
)
|
||||
|
||||
type NewMCFunc = func(json.RawMessage, mWAClient) mClient
|
||||
|
||||
var NewMC NewMCFunc
|
||||
|
||||
func (wa *WhatsAppClient) initMC() {
|
||||
if NewMC != nil {
|
||||
wa.MC = NewMC(wa.UserLogin.Metadata.(*waid.UserLoginMetadata).MData, wa)
|
||||
}
|
||||
}
|
||||
|
||||
type mClient = interface {
|
||||
OnConnect(version uint32, platform string)
|
||||
}
|
||||
|
||||
type noopMC struct{}
|
||||
|
||||
var noopMCInstance mClient = &noopMC{}
|
||||
|
||||
func (n *noopMC) OnConnect(version uint32, platform string) {}
|
||||
|
||||
type mWAClient = interface {
|
||||
MSend(data []byte)
|
||||
MSave(data json.RawMessage)
|
||||
}
|
||||
|
||||
var _ mWAClient = (*WhatsAppClient)(nil)
|
||||
|
||||
// Deprecated: ignore DangerousInternal error
|
||||
func (wa *WhatsAppClient) MSend(bytes []byte) {
|
||||
_, err := wa.Client.DangerousInternals().SendIQAsync(wa.Main.Bridge.BackgroundCtx, whatsmeow.DangerousInfoQuery{
|
||||
Namespace: "w:stats",
|
||||
Type: "set",
|
||||
To: types.ServerJID,
|
||||
Content: []waBinary.Node{{
|
||||
Tag: "add",
|
||||
Attrs: waBinary.Attrs{"t": time.Now().Unix()},
|
||||
Content: bytes,
|
||||
}},
|
||||
})
|
||||
if err != nil {
|
||||
wa.UserLogin.Log.Err(err).Msg("Failed to send stats")
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) MSave(s json.RawMessage) {
|
||||
wa.UserLogin.Metadata.(*waid.UserLoginMetadata).MData = s
|
||||
err := wa.UserLogin.Save(context.Background())
|
||||
if err != nil {
|
||||
wa.UserLogin.Log.Err(err).Msg("Failed to save MC data")
|
||||
}
|
||||
}
|
||||
|
|
@ -137,7 +137,7 @@ func (wa *WhatsAppClient) sendMediaRequest(ctx context.Context, req *wadb.MediaR
|
|||
req.Status = wadb.MediaBackfillRequestStatusRequestSkipped
|
||||
return
|
||||
}
|
||||
err = wa.sendMediaRequestDirect(ctx, req.MessageID, req.MediaKey)
|
||||
err = wa.sendMediaRequestDirect(req.MessageID, req.MediaKey)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to send media retry request")
|
||||
req.Status = wadb.MediaBackfillRequestStatusRequestFailed
|
||||
|
|
@ -148,12 +148,12 @@ func (wa *WhatsAppClient) sendMediaRequest(ctx context.Context, req *wadb.MediaR
|
|||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) sendMediaRequestDirect(ctx context.Context, rawMsgID networkid.MessageID, key []byte) error {
|
||||
func (wa *WhatsAppClient) sendMediaRequestDirect(rawMsgID networkid.MessageID, key []byte) error {
|
||||
msgID, err := waid.ParseMessageID(rawMsgID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to parse message ID: %w", err)
|
||||
}
|
||||
return wa.Client.SendMediaRetryReceipt(ctx, &types.MessageInfo{
|
||||
return wa.Client.SendMediaRetryReceipt(&types.MessageInfo{
|
||||
ID: msgID.ID,
|
||||
MessageSource: types.MessageSource{
|
||||
IsFromMe: msgID.Sender.User == wa.JID.User,
|
||||
|
|
|
|||
|
|
@ -18,26 +18,50 @@ package connector
|
|||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"go.mau.fi/util/exfmt"
|
||||
"go.mau.fi/util/jsontime"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/proto/waE2E"
|
||||
"maunium.net/go/mautrix/bridgev2/status"
|
||||
"maunium.net/go/mautrix/event"
|
||||
|
||||
"go.mau.fi/mautrix-whatsapp/pkg/waid"
|
||||
)
|
||||
|
||||
var _ status.BridgeStateFiller = (*WhatsAppClient)(nil)
|
||||
|
||||
func (wa *WhatsAppClient) FillBridgeState(state status.BridgeState) status.BridgeState {
|
||||
if !wa.PhoneRecentlySeen(false) && state.StateEvent == status.StateConnected {
|
||||
// TODO transient disconnect is wrong, this should be bad credentials or connected
|
||||
state.StateEvent = status.StateTransientDisconnect
|
||||
state.Error = WAPhoneOffline
|
||||
state.UserAction = status.UserActionOpenNative
|
||||
func (wa *WhatsAppClient) disconnectWarningLoop(ctx context.Context) {
|
||||
ticker := time.NewTicker(1 * time.Hour)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
if wa.Client != nil && wa.Client.IsConnected() {
|
||||
if !wa.PhoneRecentlySeen(true) {
|
||||
go wa.sendPhoneOfflineWarning(ctx)
|
||||
}
|
||||
return state
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) sendPhoneOfflineWarning(ctx context.Context) {
|
||||
if wa.UserLogin.User.ManagementRoom == "" || time.Since(wa.lastPhoneOfflineWarning) < 12*time.Hour {
|
||||
// Don't spam the warning too much
|
||||
return
|
||||
}
|
||||
wa.lastPhoneOfflineWarning = time.Now()
|
||||
timeSinceSeen := time.Since(wa.UserLogin.Metadata.(*waid.UserLoginMetadata).PhoneLastSeen.Time).Round(time.Hour)
|
||||
// TODO remove this manual message after bridge states are plumbed to the management room as messages
|
||||
_, _ = wa.Main.Bridge.Bot.SendMessage(ctx, wa.UserLogin.User.ManagementRoom, event.EventMessage, &event.Content{
|
||||
Parsed: &event.MessageEventContent{
|
||||
MsgType: event.MsgText,
|
||||
Body: fmt.Sprintf("Your phone hasn't been seen in %s. The server will force the bridge to log out if the phone is not active at least every 2 weeks.", exfmt.Duration(timeSinceSeen)),
|
||||
},
|
||||
}, nil)
|
||||
}
|
||||
|
||||
const PhoneDisconnectWarningTime = 12 * 24 * time.Hour // 12 days
|
||||
|
|
@ -104,10 +128,7 @@ func (wa *WhatsAppClient) phoneSeen(ts time.Time) {
|
|||
// The last seen timestamp isn't going to be perfectly accurate in any case,
|
||||
// so don't spam the database with an update every time there's an event.
|
||||
return
|
||||
}
|
||||
hadBeenSeen := wa.PhoneRecentlySeen(false)
|
||||
meta.PhoneLastSeen = jsontime.U(ts)
|
||||
if !hadBeenSeen {
|
||||
} else if !wa.PhoneRecentlySeen(false) {
|
||||
isConnected := wa.IsLoggedIn() && wa.Client.IsConnected()
|
||||
prevStateError := wa.UserLogin.BridgeState.GetPrev().Error
|
||||
if prevStateError == WAPhoneOffline && isConnected {
|
||||
|
|
@ -120,6 +141,7 @@ func (wa *WhatsAppClient) phoneSeen(ts time.Time) {
|
|||
Msg("Saw phone after current bridge state said it has been offline, not sending new bridge state")
|
||||
}
|
||||
}
|
||||
meta.PhoneLastSeen = jsontime.U(ts)
|
||||
go func() {
|
||||
err := wa.UserLogin.Save(ctx)
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -58,12 +58,7 @@ func (wa *WhatsAppConnector) updateProxy(ctx context.Context, client *whatsmeow.
|
|||
}
|
||||
if proxy, err := wa.getProxy(reason); err != nil {
|
||||
return fmt.Errorf("failed to get proxy address: %w", err)
|
||||
} else if proxy == "" {
|
||||
return nil
|
||||
} else if err = client.SetProxyAddress(proxy, whatsmeow.SetProxyOptions{
|
||||
OnlyLogin: wa.Config.ProxyOnlyLogin,
|
||||
NoMedia: wa.Config.ProxyOnlyLogin,
|
||||
}); err != nil {
|
||||
} else if err = client.SetProxyAddress(proxy); err != nil {
|
||||
return fmt.Errorf("failed to set proxy address: %w", err)
|
||||
}
|
||||
zerolog.Ctx(ctx).Debug().Msg("Enabled proxy")
|
||||
|
|
|
|||
|
|
@ -18,24 +18,15 @@ package connector
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/ptr"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/database"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
"go.mau.fi/mautrix-whatsapp/pkg/msgconv"
|
||||
"go.mau.fi/mautrix-whatsapp/pkg/waid"
|
||||
)
|
||||
|
||||
|
|
@ -44,7 +35,6 @@ var (
|
|||
_ bridgev2.ContactListingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.UserSearchingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.GhostDMCreatingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.GroupCreatingNetworkAPI = (*WhatsAppClient)(nil)
|
||||
_ bridgev2.IdentifierValidatingNetwork = (*WhatsAppConnector)(nil)
|
||||
)
|
||||
|
||||
|
|
@ -62,11 +52,9 @@ func looksEmaily(str string) bool {
|
|||
return false
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) validateIdentifer(ctx context.Context, number string) (types.JID, error) {
|
||||
if strings.HasSuffix(number, "@"+types.BotServer) || strings.HasSuffix(number, "@"+types.HiddenUserServer) {
|
||||
func (wa *WhatsAppClient) validateIdentifer(number string) (types.JID, error) {
|
||||
if strings.HasSuffix(number, "@"+types.BotServer) {
|
||||
return types.ParseJID(number)
|
||||
} else if strings.HasPrefix(number, waid.BotPrefix) || strings.HasPrefix(number, waid.LIDPrefix) {
|
||||
return waid.ParseUserID(networkid.UserID(number)), nil
|
||||
}
|
||||
if strings.HasSuffix(number, "@"+types.DefaultUserServer) {
|
||||
jid, _ := types.ParseJID(number)
|
||||
|
|
@ -76,7 +64,7 @@ func (wa *WhatsAppClient) validateIdentifer(ctx context.Context, number string)
|
|||
return types.EmptyJID, ErrInputLooksLikeEmail
|
||||
} else if wa.Client == nil || !wa.Client.IsLoggedIn() {
|
||||
return types.EmptyJID, bridgev2.ErrNotLoggedIn
|
||||
} else if resp, err := wa.Client.IsOnWhatsApp(ctx, []string{number}); err != nil {
|
||||
} else if resp, err := wa.Client.IsOnWhatsApp([]string{number}); err != nil {
|
||||
return types.EmptyJID, fmt.Errorf("failed to check if number is on WhatsApp: %w", err)
|
||||
} else if len(resp) == 0 {
|
||||
return types.EmptyJID, fmt.Errorf("the server did not respond to the query")
|
||||
|
|
@ -108,47 +96,12 @@ func (wa *WhatsAppConnector) ValidateUserID(id networkid.UserID) bool {
|
|||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) startChatLIDToPN(ctx context.Context, jid types.JID) (types.JID, error) {
|
||||
if jid.Server == types.HiddenUserServer {
|
||||
pn, err := wa.GetStore().LIDs.GetPNForLID(ctx, jid)
|
||||
if err != nil {
|
||||
return jid, fmt.Errorf("failed to get phone number for lid: %w", err)
|
||||
} else if pn.IsEmpty() {
|
||||
// Don't allow starting chats with LIDs for now
|
||||
return jid, fmt.Errorf("phone number not found")
|
||||
}
|
||||
return pn, nil
|
||||
}
|
||||
return jid, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) makeCreateChatResponse(ctx context.Context, jid, origJID types.JID) *bridgev2.CreateChatResponse {
|
||||
var redirID networkid.UserID
|
||||
if origJID != jid {
|
||||
redirID = waid.MakeUserID(jid)
|
||||
}
|
||||
return &bridgev2.CreateChatResponse{
|
||||
PortalKey: wa.makeWAPortalKey(jid),
|
||||
PortalInfo: wa.wrapDMInfo(ctx, jid),
|
||||
DMRedirectedTo: redirID,
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) CreateChatWithGhost(ctx context.Context, ghost *bridgev2.Ghost) (*bridgev2.CreateChatResponse, error) {
|
||||
origJID := waid.ParseUserID(ghost.ID)
|
||||
jid, err := wa.startChatLIDToPN(ctx, origJID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return wa.makeCreateChatResponse(ctx, jid, origJID), nil
|
||||
return &bridgev2.CreateChatResponse{PortalKey: wa.makeWAPortalKey(waid.ParseUserID(ghost.ID))}, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) ResolveIdentifier(ctx context.Context, identifier string, startChat bool) (*bridgev2.ResolveIdentifierResponse, error) {
|
||||
origJID, err := wa.validateIdentifer(ctx, identifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
jid, err := wa.startChatLIDToPN(ctx, origJID)
|
||||
jid, err := wa.validateIdentifer(identifier)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -160,16 +113,16 @@ func (wa *WhatsAppClient) ResolveIdentifier(ctx context.Context, identifier stri
|
|||
return &bridgev2.ResolveIdentifierResponse{
|
||||
Ghost: ghost,
|
||||
UserID: waid.MakeUserID(jid),
|
||||
Chat: wa.makeCreateChatResponse(ctx, jid, origJID),
|
||||
Chat: &bridgev2.CreateChatResponse{PortalKey: wa.makeWAPortalKey(jid)},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) GetContactList(ctx context.Context) ([]*bridgev2.ResolveIdentifierResponse, error) {
|
||||
return wa.getContactList(ctx, "", true)
|
||||
return wa.getContactList(ctx, "")
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) SearchUsers(ctx context.Context, query string) ([]*bridgev2.ResolveIdentifierResponse, error) {
|
||||
return wa.getContactList(ctx, strings.ToLower(query), false)
|
||||
return wa.getContactList(ctx, strings.ToLower(query))
|
||||
}
|
||||
|
||||
func matchesQuery(str string, query string) bool {
|
||||
|
|
@ -179,19 +132,16 @@ func matchesQuery(str string, query string) bool {
|
|||
return strings.Contains(strings.ToLower(str), query)
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) getContactList(ctx context.Context, filter string, onlyContacts bool) ([]*bridgev2.ResolveIdentifierResponse, error) {
|
||||
func (wa *WhatsAppClient) getContactList(ctx context.Context, filter string) ([]*bridgev2.ResolveIdentifierResponse, error) {
|
||||
if !wa.IsLoggedIn() {
|
||||
return nil, mautrix.MForbidden.WithMessage("You must be logged in to list contacts")
|
||||
}
|
||||
contacts, err := wa.GetStore().Contacts.GetAllContacts(ctx)
|
||||
contacts, err := wa.GetStore().Contacts.GetAllContacts()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
resp := make([]*bridgev2.ResolveIdentifierResponse, 0, len(contacts))
|
||||
for jid, contactInfo := range contacts {
|
||||
if onlyContacts && (contactInfo.FirstName == "" && contactInfo.FullName == "") {
|
||||
continue
|
||||
}
|
||||
if !matchesQuery(contactInfo.PushName, filter) && !matchesQuery(contactInfo.FullName, filter) && !matchesQuery(jid.User, filter) {
|
||||
continue
|
||||
}
|
||||
|
|
@ -205,165 +155,3 @@ func (wa *WhatsAppClient) getContactList(ctx context.Context, filter string, onl
|
|||
}
|
||||
return resp, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) CreateGroup(ctx context.Context, params *bridgev2.GroupCreateParams) (*bridgev2.CreateChatResponse, error) {
|
||||
createKey := wa.Client.GenerateMessageID()
|
||||
if params.RoomID != "" {
|
||||
wa.createDedup.Add(createKey)
|
||||
}
|
||||
req := whatsmeow.ReqCreateGroup{
|
||||
Name: ptr.Val(params.Name).Name,
|
||||
Participants: make([]types.JID, len(params.Participants)),
|
||||
CreateKey: createKey,
|
||||
}
|
||||
for i, participant := range params.Participants {
|
||||
jid := waid.ParseUserID(participant)
|
||||
// Normalize to PN if it's a LID
|
||||
jid, err := wa.startChatLIDToPN(ctx, jid)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to normalize participant %s: %w", participant, err)
|
||||
}
|
||||
req.Participants[i] = jid
|
||||
}
|
||||
if params.Parent != nil {
|
||||
var err error
|
||||
req.GroupLinkedParent.LinkedParentJID, err = waid.ParsePortalID(params.Parent.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse parent ID: %w", err)
|
||||
}
|
||||
}
|
||||
if params.Disappear != nil {
|
||||
req.GroupEphemeral = types.GroupEphemeral{
|
||||
IsEphemeral: true,
|
||||
DisappearingTimer: uint32(params.Disappear.Timer.Seconds()),
|
||||
}
|
||||
}
|
||||
var avatarBytes []byte
|
||||
var avatarMXC id.ContentURIString
|
||||
if params.Avatar != nil && params.Avatar.URL != "" {
|
||||
avatarMXC = params.Avatar.URL
|
||||
var err error
|
||||
avatarBytes, err = wa.Main.Bridge.Bot.DownloadMedia(ctx, params.Avatar.URL, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to download avatar: %w", err)
|
||||
}
|
||||
avatarBytes, err = convertRoomAvatar(avatarBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
resp, err := wa.Client.CreateGroup(ctx, req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create group: %w", err)
|
||||
}
|
||||
failedParticipants := make(map[networkid.UserID]*bridgev2.CreateChatFailedParticipant)
|
||||
filteredParticipants := resp.Participants[:0]
|
||||
for _, pcp := range resp.Participants {
|
||||
if pcp.Error != 0 {
|
||||
var inviteContent *event.Content
|
||||
if pcp.AddRequest != nil {
|
||||
inviteContent = &event.Content{
|
||||
Raw: map[string]any{
|
||||
msgconv.GroupInviteMetaField: &waid.GroupInviteMeta{
|
||||
JID: resp.JID,
|
||||
Code: pcp.AddRequest.Code,
|
||||
Expiration: pcp.AddRequest.Expiration.Unix(),
|
||||
Inviter: wa.JID.ToNonAD(),
|
||||
GroupName: resp.Name,
|
||||
IsParentGroup: resp.IsParent,
|
||||
},
|
||||
},
|
||||
Parsed: &event.MessageEventContent{
|
||||
Body: "Invitation to join my WhatsApp group",
|
||||
MsgType: event.MsgText,
|
||||
},
|
||||
}
|
||||
}
|
||||
failedParticipants[waid.MakeUserID(pcp.JID)] = &bridgev2.CreateChatFailedParticipant{
|
||||
Reason: fmt.Sprintf("error %d", pcp.Error),
|
||||
InviteEventType: event.EventMessage.Type,
|
||||
InviteContent: inviteContent,
|
||||
}
|
||||
} else {
|
||||
filteredParticipants = append(filteredParticipants, pcp)
|
||||
}
|
||||
}
|
||||
resp.Participants = filteredParticipants
|
||||
portal, err := wa.Main.Bridge.GetPortalByKey(ctx, wa.makeWAPortalKey(resp.JID))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get portal: %w", err)
|
||||
}
|
||||
groupInfo := wa.wrapGroupInfo(ctx, resp)
|
||||
if params.RoomID != "" {
|
||||
err = portal.UpdateMatrixRoomID(ctx, params.RoomID, bridgev2.UpdateMatrixRoomIDParams{
|
||||
SyncDBMetadata: func() {
|
||||
portal.Name = req.Name
|
||||
portal.NameSet = true
|
||||
portal.ParentKey = ptr.Val(params.Parent)
|
||||
if avatarBytes != nil {
|
||||
portal.AvatarSet = true
|
||||
portal.AvatarHash = sha256.Sum256(avatarBytes)
|
||||
portal.AvatarMXC = avatarMXC
|
||||
}
|
||||
if req.DisappearingTimer > 0 {
|
||||
portal.Disappear = database.DisappearingSetting{
|
||||
Type: event.DisappearingTypeAfterSend,
|
||||
Timer: time.Duration(req.DisappearingTimer) * time.Second,
|
||||
}
|
||||
}
|
||||
},
|
||||
OverwriteOldPortal: true,
|
||||
TombstoneOldRoom: true,
|
||||
DeleteOldRoom: true,
|
||||
ChatInfo: groupInfo,
|
||||
ChatInfoSource: wa.UserLogin,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to update room ID after creating group: %w", err)
|
||||
}
|
||||
}
|
||||
changed := false
|
||||
if avatarBytes != nil {
|
||||
avatarID, err := wa.Client.SetGroupPhoto(ctx, resp.JID, avatarBytes)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to set group avatar after creating group")
|
||||
} else {
|
||||
portal.AvatarID = networkid.AvatarID(avatarID)
|
||||
portal.AvatarHash = sha256.Sum256(avatarBytes)
|
||||
portal.AvatarMXC = avatarMXC
|
||||
portal.AvatarSet = true
|
||||
groupInfo.Avatar = &bridgev2.Avatar{
|
||||
ID: portal.AvatarID,
|
||||
MXC: portal.AvatarMXC,
|
||||
Hash: portal.AvatarHash,
|
||||
}
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
if params.Topic != nil {
|
||||
newTopicID := wa.Client.GenerateMessageID()
|
||||
err = wa.Client.SetGroupTopic(ctx, resp.JID, "", newTopicID, params.Topic.Topic)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to set group topic after creating group")
|
||||
} else {
|
||||
portal.Topic = params.Topic.Topic
|
||||
portal.TopicSet = params.RoomID != ""
|
||||
portal.Metadata.(*waid.PortalMetadata).TopicID = newTopicID
|
||||
changed = true
|
||||
groupInfo.Topic = ¶ms.Topic.Topic
|
||||
}
|
||||
}
|
||||
if changed {
|
||||
err = portal.Save(ctx)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to save portal after post-creation updates")
|
||||
}
|
||||
}
|
||||
return &bridgev2.CreateChatResponse{
|
||||
PortalKey: wa.makeWAPortalKey(resp.JID),
|
||||
Portal: portal,
|
||||
PortalInfo: groupInfo,
|
||||
|
||||
FailedParticipants: failedParticipants,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,12 +2,9 @@ package connector
|
|||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math/rand/v2"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
|
|
@ -20,39 +17,31 @@ import (
|
|||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
"maunium.net/go/mautrix/bridgev2/simplevent"
|
||||
|
||||
"go.mau.fi/mautrix-whatsapp/pkg/connector/wadb"
|
||||
"go.mau.fi/mautrix-whatsapp/pkg/waid"
|
||||
)
|
||||
|
||||
var ResyncMinInterval = 7 * 24 * time.Hour
|
||||
var ResyncLoopInterval = 4 * time.Hour
|
||||
var ResyncJitterSeconds = 3600
|
||||
const resyncMinInterval = 7 * 24 * time.Hour
|
||||
const resyncLoopInterval = 4 * time.Hour
|
||||
|
||||
func (wa *WhatsAppClient) EnqueueGhostResync(ghost *bridgev2.Ghost) {
|
||||
if ghost.Metadata.(*waid.GhostMetadata).LastSync.Add(ResyncMinInterval).After(time.Now()) {
|
||||
if ghost.Metadata.(*waid.GhostMetadata).LastSync.Add(resyncMinInterval).After(time.Now()) {
|
||||
return
|
||||
}
|
||||
wa.resyncQueueLock.Lock()
|
||||
jid := waid.ParseUserID(ghost.ID)
|
||||
if _, exists := wa.resyncQueue[jid]; !exists {
|
||||
wa.resyncQueue[jid] = resyncQueueItem{ghost: ghost}
|
||||
nextResyncIn := time.Until(wa.nextResync).String()
|
||||
if wa.nextResync.IsZero() {
|
||||
nextResyncIn = "never"
|
||||
}
|
||||
wa.UserLogin.Log.Debug().
|
||||
Stringer("jid", jid).
|
||||
Str("next_resync_in", nextResyncIn).
|
||||
Stringer("next_resync_in", time.Until(wa.nextResync)).
|
||||
Msg("Enqueued resync for ghost")
|
||||
}
|
||||
wa.resyncQueueLock.Unlock()
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) EnqueuePortalResync(portal *bridgev2.Portal, allowDM bool) {
|
||||
func (wa *WhatsAppClient) EnqueuePortalResync(portal *bridgev2.Portal) {
|
||||
jid, _ := waid.ParsePortalID(portal.ID)
|
||||
if portal.Metadata.(*waid.PortalMetadata).LastSync.Add(ResyncMinInterval).After(time.Now()) {
|
||||
return
|
||||
} else if !allowDM && jid.Server != types.GroupServer {
|
||||
if jid.Server != types.GroupServer || portal.Metadata.(*waid.PortalMetadata).LastSync.Add(resyncMinInterval).After(time.Now()) {
|
||||
return
|
||||
}
|
||||
wa.resyncQueueLock.Lock()
|
||||
|
|
@ -69,7 +58,7 @@ func (wa *WhatsAppClient) EnqueuePortalResync(portal *bridgev2.Portal, allowDM b
|
|||
func (wa *WhatsAppClient) ghostResyncLoop(ctx context.Context) {
|
||||
log := wa.UserLogin.Log.With().Str("action", "ghost resync loop").Logger()
|
||||
ctx = log.WithContext(ctx)
|
||||
wa.nextResync = time.Now().Add(ResyncLoopInterval).Add(-time.Duration(rand.IntN(ResyncJitterSeconds)) * time.Second)
|
||||
wa.nextResync = time.Now().Add(resyncLoopInterval).Add(-time.Duration(rand.IntN(3600)) * time.Second)
|
||||
timer := time.NewTimer(time.Until(wa.nextResync))
|
||||
log.Info().Time("first_resync", wa.nextResync).Msg("Ghost resync queue starting")
|
||||
for {
|
||||
|
|
@ -92,7 +81,7 @@ func (wa *WhatsAppClient) ghostResyncLoop(ctx context.Context) {
|
|||
func (wa *WhatsAppClient) rotateResyncQueue() map[types.JID]resyncQueueItem {
|
||||
wa.resyncQueueLock.Lock()
|
||||
defer wa.resyncQueueLock.Unlock()
|
||||
wa.nextResync = time.Now().Add(ResyncLoopInterval)
|
||||
wa.nextResync = time.Now().Add(resyncLoopInterval)
|
||||
if len(wa.resyncQueue) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -119,7 +108,7 @@ func (wa *WhatsAppClient) doGhostResync(ctx context.Context, queue map[types.JID
|
|||
} else if item.portal != nil {
|
||||
lastSync = item.portal.Metadata.(*waid.PortalMetadata).LastSync.Time
|
||||
}
|
||||
if lastSync.Add(ResyncMinInterval).After(time.Now()) {
|
||||
if lastSync.Add(resyncMinInterval).After(time.Now()) {
|
||||
log.Debug().
|
||||
Stringer("jid", jid).
|
||||
Time("last_sync", lastSync).
|
||||
|
|
@ -134,7 +123,7 @@ func (wa *WhatsAppClient) doGhostResync(ctx context.Context, queue map[types.JID
|
|||
}
|
||||
}
|
||||
for _, portal := range portals {
|
||||
wa.UserLogin.QueueRemoteEvent(&simplevent.ChatResync{
|
||||
wa.Main.Bridge.QueueRemoteEvent(wa.UserLogin, &simplevent.ChatResync{
|
||||
EventMeta: simplevent.EventMeta{
|
||||
Type: bridgev2.RemoteEventChatResync,
|
||||
LogContext: func(c zerolog.Context) zerolog.Context {
|
||||
|
|
@ -149,7 +138,7 @@ func (wa *WhatsAppClient) doGhostResync(ctx context.Context, queue map[types.JID
|
|||
return
|
||||
}
|
||||
log.Debug().Array("jids", exzerolog.ArrayOfStringers(ghostJIDs)).Msg("Doing background sync for users")
|
||||
infos, err := wa.Client.GetUserInfo(ctx, ghostJIDs)
|
||||
infos, err := wa.Client.GetUserInfo(ghostJIDs)
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to get user info for background sync")
|
||||
return
|
||||
|
|
@ -167,12 +156,11 @@ func (wa *WhatsAppClient) doGhostResync(ctx context.Context, queue map[types.JID
|
|||
continue
|
||||
}
|
||||
ghost.UpdateInfo(ctx, userInfo)
|
||||
wa.syncAltGhostWithInfo(ctx, jid, userInfo)
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) GetUserInfo(ctx context.Context, ghost *bridgev2.Ghost) (*bridgev2.UserInfo, error) {
|
||||
if ghost.Name != "" && ghost.NameSet {
|
||||
if ghost.Name != "" {
|
||||
wa.EnqueueGhostResync(ghost)
|
||||
return nil, nil
|
||||
}
|
||||
|
|
@ -181,7 +169,7 @@ func (wa *WhatsAppClient) GetUserInfo(ctx context.Context, ghost *bridgev2.Ghost
|
|||
}
|
||||
|
||||
func (wa *WhatsAppClient) getUserInfo(ctx context.Context, jid types.JID, fetchAvatar bool) (*bridgev2.UserInfo, error) {
|
||||
contact, err := wa.GetStore().Contacts.GetContact(ctx, jid)
|
||||
contact, err := wa.GetStore().Contacts.GetContact(jid)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -191,80 +179,48 @@ func (wa *WhatsAppClient) getUserInfo(ctx context.Context, jid types.JID, fetchA
|
|||
func (wa *WhatsAppClient) contactToUserInfo(ctx context.Context, jid types.JID, contact types.ContactInfo, getAvatar bool) *bridgev2.UserInfo {
|
||||
if jid == types.MetaAIJID && contact.PushName == jid.User {
|
||||
contact.PushName = "Meta AI"
|
||||
} else if jid == types.LegacyPSAJID || jid == types.PSAJID {
|
||||
} else if jid == types.PSAJID {
|
||||
contact.PushName = "WhatsApp"
|
||||
}
|
||||
var altJID types.JID
|
||||
if jid.Server == types.DefaultUserServer || jid.Server == types.HiddenUserServer {
|
||||
var err error
|
||||
altJID, err = wa.GetStore().GetAltJID(ctx, jid)
|
||||
var phone string
|
||||
if jid.Server == types.DefaultUserServer {
|
||||
phone = "+" + jid.User
|
||||
} else if jid.Server == types.HiddenUserServer {
|
||||
pnJID, err := wa.GetStore().LIDs.GetPNForLID(ctx, jid)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Stringer("source_jid", jid).Msg("Failed to get alt JID")
|
||||
} else if altJID.IsEmpty() {
|
||||
zerolog.Ctx(ctx).Debug().Stringer("source_jid", jid).Msg("Alternate JID not found in contactToUserInfo")
|
||||
zerolog.Ctx(ctx).Err(err).Stringer("lid", jid).Msg("Failed to get PN for LID")
|
||||
} else {
|
||||
extraContact, err := wa.GetStore().Contacts.GetContact(ctx, altJID)
|
||||
phone = "+" + pnJID.User
|
||||
extraContact, err := wa.GetStore().Contacts.GetContact(pnJID)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).
|
||||
Stringer("source_jid", jid).
|
||||
Stringer("alt_jid", altJID).
|
||||
Msg("Failed to get contact info from alternate JID")
|
||||
Stringer("lid", jid).
|
||||
Stringer("pn_jid", pnJID).
|
||||
Msg("Failed to get contact info from PN")
|
||||
} else {
|
||||
// Phone contact info should only be stored for phone number JIDs
|
||||
if altJID.Server == types.DefaultUserServer {
|
||||
if contact.FirstName == "" {
|
||||
contact.FirstName = extraContact.FirstName
|
||||
}
|
||||
if contact.FullName == "" {
|
||||
contact.FullName = extraContact.FullName
|
||||
}
|
||||
}
|
||||
if contact.PushName == "" {
|
||||
contact.PushName = extraContact.PushName
|
||||
}
|
||||
if contact.BusinessName == "" {
|
||||
contact.BusinessName = extraContact.BusinessName
|
||||
}
|
||||
if contact.PushName != "" && extraContact.PushName != "" && contact.PushName != extraContact.PushName {
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Stringer("source_jid", jid).
|
||||
Stringer("alt_jid", altJID).
|
||||
Str("source_push_name", contact.PushName).
|
||||
Str("alt_push_name", extraContact.PushName).
|
||||
Msg("Conflicting push names between JIDs")
|
||||
if altJID.Server == types.DefaultUserServer {
|
||||
contact.PushName = extraContact.PushName
|
||||
}
|
||||
}
|
||||
if contact.BusinessName != "" && extraContact.BusinessName != "" && contact.BusinessName != extraContact.BusinessName {
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Stringer("source_jid", jid).
|
||||
Stringer("alt_jid", altJID).
|
||||
Str("source_push_name", contact.BusinessName).
|
||||
Str("alt_push_name", extraContact.BusinessName).
|
||||
Msg("Conflicting business names between JIDs")
|
||||
if altJID.Server == types.DefaultUserServer {
|
||||
contact.BusinessName = extraContact.BusinessName
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var phone string
|
||||
if jid.Server == types.DefaultUserServer {
|
||||
phone = "+" + jid.User
|
||||
} else if altJID.Server == types.DefaultUserServer {
|
||||
phone = "+" + altJID.User
|
||||
}
|
||||
ui := &bridgev2.UserInfo{
|
||||
Name: ptr.Ptr(wa.Main.Config.FormatDisplayname(jid, phone, contact)),
|
||||
IsBot: ptr.Ptr(jid.IsBot()),
|
||||
Identifiers: []string{fmt.Sprintf("tel:+%s", jid.User)},
|
||||
ExtraUpdates: updateGhostLastSyncAt,
|
||||
}
|
||||
if jid.Server == types.BotServer {
|
||||
ui.Identifiers = []string{}
|
||||
} else if phone != "" {
|
||||
ui.Identifiers = []string{fmt.Sprintf("tel:%s", phone)}
|
||||
}
|
||||
if getAvatar {
|
||||
ui.ExtraUpdates = bridgev2.MergeExtraUpdaters(ui.ExtraUpdates, wa.fetchGhostAvatar)
|
||||
|
|
@ -274,56 +230,11 @@ func (wa *WhatsAppClient) contactToUserInfo(ctx context.Context, jid types.JID,
|
|||
|
||||
func updateGhostLastSyncAt(_ context.Context, ghost *bridgev2.Ghost) bool {
|
||||
meta := ghost.Metadata.(*waid.GhostMetadata)
|
||||
forceSave := ResyncMinInterval < 24*time.Hour || time.Since(meta.LastSync.Time) > 24*time.Hour
|
||||
forceSave := time.Since(meta.LastSync.Time) > 24*time.Hour
|
||||
meta.LastSync = jsontime.UnixNow()
|
||||
return forceSave
|
||||
}
|
||||
|
||||
var expiryRegex = regexp.MustCompile("oe=([0-9A-Fa-f]+)")
|
||||
|
||||
func avatarInfoToCacheEntry(ctx context.Context, jid types.JID, avatar *types.ProfilePictureInfo) *wadb.AvatarCacheEntry {
|
||||
expiry := time.Now().Add(24 * time.Hour)
|
||||
match := expiryRegex.FindStringSubmatch(avatar.DirectPath)
|
||||
if len(match) == 2 {
|
||||
expiryUnix, err := strconv.ParseInt(match[1], 16, 64)
|
||||
if err == nil {
|
||||
expiry = time.Unix(expiryUnix, 0)
|
||||
} else {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).
|
||||
Strs("match", match).
|
||||
Msg("Failed to parse expiry from avatar direct path")
|
||||
}
|
||||
}
|
||||
return &wadb.AvatarCacheEntry{
|
||||
EntityJID: jid,
|
||||
AvatarID: avatar.ID,
|
||||
DirectPath: avatar.DirectPath,
|
||||
Expiry: jsontime.U(expiry),
|
||||
Gone: false,
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) makeDirectMediaAvatar(ctx context.Context, jid types.JID, avatar *types.ProfilePictureInfo, community bool) (*bridgev2.Avatar, error) {
|
||||
mxc, err := wa.Main.Bridge.Matrix.GenerateContentURI(ctx, waid.MakeAvatarMediaID(jid, avatar.ID, wa.UserLogin.ID, community))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate MXC URI: %w", err)
|
||||
}
|
||||
cacheEntry := avatarInfoToCacheEntry(ctx, jid, avatar)
|
||||
err = wa.Main.DB.AvatarCache.Put(ctx, cacheEntry)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to cache avatar info: %w", err)
|
||||
}
|
||||
hash := sha256.Sum256([]byte(avatar.ID))
|
||||
if len(avatar.Hash) == 32 {
|
||||
hash = [32]byte(avatar.Hash)
|
||||
}
|
||||
return &bridgev2.Avatar{
|
||||
ID: networkid.AvatarID(avatar.ID),
|
||||
MXC: mxc,
|
||||
Hash: hash,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) fetchGhostAvatar(ctx context.Context, ghost *bridgev2.Ghost) bool {
|
||||
jid := waid.ParseUserID(ghost.ID)
|
||||
existingID := string(ghost.AvatarID)
|
||||
|
|
@ -331,7 +242,7 @@ func (wa *WhatsAppClient) fetchGhostAvatar(ctx context.Context, ghost *bridgev2.
|
|||
existingID = ""
|
||||
}
|
||||
var wrappedAvatar *bridgev2.Avatar
|
||||
avatar, err := wa.Client.GetProfilePictureInfo(ctx, jid, &whatsmeow.GetProfilePictureParams{ExistingID: existingID})
|
||||
avatar, err := wa.Client.GetProfilePictureInfo(jid, &whatsmeow.GetProfilePictureParams{ExistingID: existingID})
|
||||
if errors.Is(err, whatsmeow.ErrProfilePictureNotSet) {
|
||||
wrappedAvatar = &bridgev2.Avatar{
|
||||
ID: "remove",
|
||||
|
|
@ -347,90 +258,32 @@ func (wa *WhatsAppClient) fetchGhostAvatar(ctx context.Context, ghost *bridgev2.
|
|||
return false
|
||||
} else if avatar == nil {
|
||||
return false
|
||||
} else if wa.Main.MsgConv.DirectMedia {
|
||||
wrappedAvatar, err = wa.makeDirectMediaAvatar(ctx, jid, avatar, false)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to prepare direct media avatar")
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
wrappedAvatar = &bridgev2.Avatar{
|
||||
ID: networkid.AvatarID(avatar.ID),
|
||||
Get: func(ctx context.Context) ([]byte, error) {
|
||||
return wa.Client.DownloadMediaWithPath(ctx, avatar.DirectPath, nil, nil, nil, 0, "", "")
|
||||
return wa.Client.DownloadMediaWithPath(avatar.DirectPath, nil, nil, nil, 0, "", "")
|
||||
},
|
||||
}
|
||||
}
|
||||
return ghost.UpdateAvatar(ctx, wrappedAvatar)
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) resyncContacts(forceAvatarSync, automatic bool) {
|
||||
func (wa *WhatsAppClient) resyncContacts(forceAvatarSync bool) {
|
||||
log := wa.UserLogin.Log.With().Str("action", "resync contacts").Logger()
|
||||
ctx := log.WithContext(wa.Main.Bridge.BackgroundCtx)
|
||||
if automatic && wa.isNewLogin {
|
||||
log.Debug().Msg("Waiting for push name history sync before resyncing contacts")
|
||||
timeoutCtx, cancel := context.WithTimeout(ctx, 5*time.Minute)
|
||||
_ = wa.pushNamesSynced.Wait(timeoutCtx)
|
||||
cancel()
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
contactStore := wa.GetStore().Contacts
|
||||
contacts, err := contactStore.GetAllContacts(ctx)
|
||||
ctx := log.WithContext(context.Background())
|
||||
contacts, err := wa.Device.Contacts.GetAllContacts()
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to get cached contacts")
|
||||
return
|
||||
}
|
||||
log.Info().Int("contact_count", len(contacts)).Msg("Resyncing displaynames with contact info")
|
||||
for jid := range contacts {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
for jid, contact := range contacts {
|
||||
ghost, err := wa.Main.Bridge.GetGhostByID(ctx, waid.MakeUserID(jid))
|
||||
if err != nil {
|
||||
log.Err(err).Stringer("jid", jid).Msg("Failed to get ghost")
|
||||
// Refetch contact info from the store to reduce the risk of races.
|
||||
// This should always hit the cache.
|
||||
} else if contact, err := contactStore.GetContact(ctx, jid); err != nil {
|
||||
log.Err(err).Stringer("jid", jid).Msg("Failed to get contact info")
|
||||
} else {
|
||||
userInfo := wa.contactToUserInfo(ctx, jid, contact, forceAvatarSync || ghost.AvatarID == "")
|
||||
ghost.UpdateInfo(ctx, userInfo)
|
||||
wa.syncAltGhostWithInfo(ctx, jid, userInfo)
|
||||
log.Err(err).Msg("Failed to get ghost")
|
||||
} else if ghost != nil {
|
||||
ghost.UpdateInfo(ctx, wa.contactToUserInfo(ctx, jid, contact, forceAvatarSync || ghost.AvatarID == ""))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (wa *WhatsAppClient) syncAltGhostWithInfo(ctx context.Context, jid types.JID, info *bridgev2.UserInfo) {
|
||||
log := zerolog.Ctx(ctx)
|
||||
var altJID types.JID
|
||||
var err error
|
||||
if jid.Server == types.HiddenUserServer {
|
||||
altJID, err = wa.GetStore().LIDs.GetPNForLID(ctx, jid)
|
||||
} else if jid.Server == types.DefaultUserServer {
|
||||
altJID, err = wa.GetStore().LIDs.GetLIDForPN(ctx, jid)
|
||||
}
|
||||
if err != nil {
|
||||
log.Warn().Err(err).
|
||||
Stringer("jid", jid).
|
||||
Msg("Failed to get alternate JID for syncing user info")
|
||||
return
|
||||
} else if altJID.IsEmpty() {
|
||||
return
|
||||
}
|
||||
ghost, err := wa.Main.Bridge.GetGhostByID(ctx, waid.MakeUserID(altJID))
|
||||
if err != nil {
|
||||
log.Err(err).
|
||||
Stringer("alternate_jid", altJID).
|
||||
Stringer("jid", jid).
|
||||
Msg("Failed to get ghost for alternate JID")
|
||||
return
|
||||
}
|
||||
ghost.UpdateInfo(ctx, info)
|
||||
log.Debug().
|
||||
Stringer("jid", jid).
|
||||
Stringer("alternate_jid", altJID).
|
||||
Msg("Synced alternate ghost with info")
|
||||
go wa.syncRemoteProfile(ctx, ghost)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,51 +0,0 @@
|
|||
package wadb
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.mau.fi/util/dbutil"
|
||||
"go.mau.fi/util/jsontime"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
)
|
||||
|
||||
type AvatarCacheQuery struct {
|
||||
*dbutil.QueryHelper[*AvatarCacheEntry]
|
||||
}
|
||||
|
||||
const (
|
||||
getAvatarCacheEntry = `
|
||||
SELECT entity_jid, avatar_id, direct_path, expiry, gone
|
||||
FROM whatsapp_avatar_cache
|
||||
WHERE entity_jid = $1 AND avatar_id = $2
|
||||
`
|
||||
putAvatarCacheEntry = `
|
||||
INSERT INTO whatsapp_avatar_cache (entity_jid, avatar_id, direct_path, expiry, gone)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (entity_jid, avatar_id) DO UPDATE
|
||||
SET direct_path = EXCLUDED.direct_path, expiry = EXCLUDED.expiry, gone = EXCLUDED.gone
|
||||
`
|
||||
)
|
||||
|
||||
func (acq *AvatarCacheQuery) Get(ctx context.Context, entityJID types.JID, avatarID string) (*AvatarCacheEntry, error) {
|
||||
return acq.QueryOne(ctx, getAvatarCacheEntry, entityJID, avatarID)
|
||||
}
|
||||
|
||||
func (acq *AvatarCacheQuery) Put(ctx context.Context, entry *AvatarCacheEntry) error {
|
||||
return acq.Exec(ctx, putAvatarCacheEntry, entry.sqlVariables()...)
|
||||
}
|
||||
|
||||
type AvatarCacheEntry struct {
|
||||
EntityJID types.JID
|
||||
AvatarID string
|
||||
DirectPath string
|
||||
Expiry jsontime.Unix
|
||||
Gone bool
|
||||
}
|
||||
|
||||
func (ace *AvatarCacheEntry) Scan(row dbutil.Scannable) (*AvatarCacheEntry, error) {
|
||||
return dbutil.ValueOrErr(ace, row.Scan(&ace.EntityJID, &ace.AvatarID, &ace.DirectPath, &ace.Expiry, &ace.Gone))
|
||||
}
|
||||
|
||||
func (ace *AvatarCacheEntry) sqlVariables() []any {
|
||||
return []any{ace.EntityJID, ace.AvatarID, ace.DirectPath, ace.Expiry, ace.Gone}
|
||||
}
|
||||
|
|
@ -6,7 +6,6 @@ import (
|
|||
"time"
|
||||
|
||||
"go.mau.fi/util/dbutil"
|
||||
"go.mau.fi/util/jsontime"
|
||||
"go.mau.fi/util/ptr"
|
||||
"go.mau.fi/whatsmeow/proto/waHistorySync"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
|
|
@ -31,6 +30,7 @@ type Conversation struct {
|
|||
EphemeralSettingTimestamp *int64
|
||||
MarkedAsUnread *bool
|
||||
UnreadCount *uint32
|
||||
Bridged bool
|
||||
}
|
||||
|
||||
func parseHistoryTime(ts *uint64) time.Time {
|
||||
|
|
@ -40,19 +40,15 @@ func parseHistoryTime(ts *uint64) time.Time {
|
|||
return time.Unix(int64(*ts), 0)
|
||||
}
|
||||
|
||||
func NewConversation(loginID networkid.UserLoginID, chatJID types.JID, conv *waHistorySync.Conversation, mostRecentMessage time.Time) *Conversation {
|
||||
func NewConversation(loginID networkid.UserLoginID, chatJID types.JID, conv *waHistorySync.Conversation) *Conversation {
|
||||
var pinned *bool
|
||||
if conv.Pinned != nil {
|
||||
pinned = ptr.Ptr(*conv.Pinned > 0)
|
||||
}
|
||||
lastMessageTS := parseHistoryTime(conv.LastMsgTimestamp)
|
||||
if lastMessageTS.IsZero() {
|
||||
lastMessageTS = mostRecentMessage
|
||||
}
|
||||
return &Conversation{
|
||||
UserLoginID: loginID,
|
||||
ChatJID: chatJID,
|
||||
LastMessageTimestamp: lastMessageTS,
|
||||
LastMessageTimestamp: parseHistoryTime(conv.LastMsgTimestamp),
|
||||
Archived: conv.Archived,
|
||||
Pinned: pinned,
|
||||
MuteEndTime: parseHistoryTime(conv.MuteEndTime),
|
||||
|
|
@ -69,9 +65,9 @@ const (
|
|||
INSERT INTO whatsapp_history_sync_conversation (
|
||||
bridge_id, user_login_id, chat_jid, last_message_timestamp, archived, pinned, mute_end_time,
|
||||
end_of_history_transfer_type, ephemeral_expiration, ephemeral_setting_timestamp, marked_as_unread,
|
||||
unread_count
|
||||
unread_count, bridged
|
||||
)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
||||
ON CONFLICT (bridge_id, user_login_id, chat_jid)
|
||||
DO UPDATE SET
|
||||
last_message_timestamp=CASE
|
||||
|
|
@ -87,15 +83,16 @@ const (
|
|||
ephemeral_expiration=COALESCE(excluded.ephemeral_expiration, whatsapp_history_sync_conversation.ephemeral_expiration),
|
||||
ephemeral_setting_timestamp=COALESCE(excluded.ephemeral_setting_timestamp, whatsapp_history_sync_conversation.ephemeral_setting_timestamp),
|
||||
marked_as_unread=COALESCE(excluded.marked_as_unread, whatsapp_history_sync_conversation.marked_as_unread),
|
||||
unread_count=COALESCE(excluded.unread_count, whatsapp_history_sync_conversation.unread_count)
|
||||
unread_count=COALESCE(excluded.unread_count, whatsapp_history_sync_conversation.unread_count),
|
||||
bridged=false
|
||||
`
|
||||
getRecentConversations = `
|
||||
SELECT
|
||||
bridge_id, user_login_id, chat_jid, last_message_timestamp, archived, pinned, mute_end_time,
|
||||
end_of_history_transfer_type, ephemeral_expiration, ephemeral_setting_timestamp, marked_as_unread,
|
||||
unread_count
|
||||
unread_count, bridged
|
||||
FROM whatsapp_history_sync_conversation
|
||||
WHERE bridge_id=$1 AND user_login_id=$2 AND (synced_login_ts IS NULL OR synced_login_ts < $4)
|
||||
WHERE bridge_id=$1 AND user_login_id=$2 AND bridged=false
|
||||
ORDER BY last_message_timestamp DESC
|
||||
LIMIT $3
|
||||
`
|
||||
|
|
@ -103,7 +100,7 @@ const (
|
|||
SELECT
|
||||
bridge_id, user_login_id, chat_jid, last_message_timestamp, archived, pinned, mute_end_time,
|
||||
end_of_history_transfer_type, ephemeral_expiration, ephemeral_setting_timestamp, marked_as_unread,
|
||||
unread_count
|
||||
unread_count, bridged
|
||||
FROM whatsapp_history_sync_conversation
|
||||
WHERE bridge_id=$1 AND user_login_id=$2 AND chat_jid=$3
|
||||
`
|
||||
|
|
@ -112,9 +109,9 @@ const (
|
|||
DELETE FROM whatsapp_history_sync_conversation
|
||||
WHERE bridge_id=$1 AND user_login_id=$2 AND chat_jid=$3
|
||||
`
|
||||
markConversationSynced = `
|
||||
markConversationBridged = `
|
||||
UPDATE whatsapp_history_sync_conversation
|
||||
SET synced_login_ts=$4
|
||||
SET bridged=true
|
||||
WHERE bridge_id=$1 AND user_login_id=$2 AND chat_jid=$3
|
||||
`
|
||||
)
|
||||
|
|
@ -124,19 +121,17 @@ func (cq *ConversationQuery) Put(ctx context.Context, conv *Conversation) error
|
|||
return cq.Exec(ctx, upsertHistorySyncConversationQuery, conv.sqlVariables()...)
|
||||
}
|
||||
|
||||
func (cq *ConversationQuery) GetRecent(
|
||||
ctx context.Context, loginID networkid.UserLoginID, limit int, notSyncedAfter jsontime.Unix,
|
||||
) ([]*Conversation, error) {
|
||||
func (cq *ConversationQuery) GetRecent(ctx context.Context, loginID networkid.UserLoginID, limit int) ([]*Conversation, error) {
|
||||
limitPtr := &limit
|
||||
// Negative limit on SQLite means unlimited, but Postgres prefers a NULL limit.
|
||||
if limit < 0 && cq.GetDB().Dialect == dbutil.Postgres {
|
||||
limitPtr = nil
|
||||
}
|
||||
return cq.QueryMany(ctx, getRecentConversations, cq.BridgeID, loginID, limitPtr, notSyncedAfter)
|
||||
return cq.QueryMany(ctx, getRecentConversations, cq.BridgeID, loginID, limitPtr)
|
||||
}
|
||||
|
||||
func (cq *ConversationQuery) MarkSynced(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID, loginTS jsontime.Unix) error {
|
||||
return cq.Exec(ctx, markConversationSynced, cq.BridgeID, loginID, chatJID, loginTS)
|
||||
func (cq *ConversationQuery) MarkBridged(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID) error {
|
||||
return cq.Exec(ctx, markConversationBridged, cq.BridgeID, loginID, chatJID)
|
||||
}
|
||||
|
||||
func (cq *ConversationQuery) Get(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID) (*Conversation, error) {
|
||||
|
|
@ -172,6 +167,7 @@ func (c *Conversation) sqlVariables() []any {
|
|||
c.EphemeralSettingTimestamp,
|
||||
c.MarkedAsUnread,
|
||||
c.UnreadCount,
|
||||
c.Bridged,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -190,6 +186,7 @@ func (c *Conversation) Scan(row dbutil.Scannable) (*Conversation, error) {
|
|||
&c.EphemeralSettingTimestamp,
|
||||
&c.MarkedAsUnread,
|
||||
&c.UnreadCount,
|
||||
&c.Bridged,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
|
|
|||
|
|
@ -14,8 +14,6 @@ type Database struct {
|
|||
Message *MessageQuery
|
||||
PollOption *PollOptionQuery
|
||||
MediaRequest *MediaRequestQuery
|
||||
HSNotif *HistorySyncNotificationQuery
|
||||
AvatarCache *AvatarCacheQuery
|
||||
}
|
||||
|
||||
func New(bridgeID networkid.BridgeID, db *dbutil.Database, log zerolog.Logger) *Database {
|
||||
|
|
@ -42,14 +40,5 @@ func New(bridgeID networkid.BridgeID, db *dbutil.Database, log zerolog.Logger) *
|
|||
return &MediaRequest{}
|
||||
}),
|
||||
},
|
||||
HSNotif: &HistorySyncNotificationQuery{
|
||||
BridgeID: bridgeID,
|
||||
Database: db,
|
||||
},
|
||||
AvatarCache: &AvatarCacheQuery{
|
||||
QueryHelper: dbutil.MakeQueryHelper(db, func(_ *dbutil.QueryHelper[*AvatarCacheEntry]) *AvatarCacheEntry {
|
||||
return &AvatarCacheEntry{}
|
||||
}),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,78 +0,0 @@
|
|||
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
|
||||
// Copyright (C) 2025 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package wadb
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
|
||||
"go.mau.fi/util/dbutil"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
|
||||
"go.mau.fi/whatsmeow/proto/waE2E"
|
||||
)
|
||||
|
||||
type HistorySyncNotificationQuery struct {
|
||||
BridgeID networkid.BridgeID
|
||||
*dbutil.Database
|
||||
}
|
||||
|
||||
const (
|
||||
putHSNotificationQuery = `
|
||||
INSERT INTO whatsapp_history_sync_notification (bridge_id, user_login_id, data)
|
||||
VALUES ($1, $2, $3)
|
||||
`
|
||||
getNextHSNotificationQuery = `
|
||||
SELECT rowid, data FROM whatsapp_history_sync_notification
|
||||
WHERE bridge_id=$1 AND user_login_id=$2
|
||||
`
|
||||
deleteHSNotificationQuery = `
|
||||
DELETE FROM whatsapp_history_sync_notification WHERE rowid=$1
|
||||
`
|
||||
)
|
||||
|
||||
func (hsnq *HistorySyncNotificationQuery) Put(ctx context.Context, loginID networkid.UserLoginID, notif *waE2E.HistorySyncNotification) error {
|
||||
notifBytes, err := proto.Marshal(notif)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = hsnq.Exec(ctx, putHSNotificationQuery, hsnq.BridgeID, loginID, notifBytes)
|
||||
return err
|
||||
}
|
||||
|
||||
func (hsnq *HistorySyncNotificationQuery) GetNext(ctx context.Context, loginID networkid.UserLoginID) (*waE2E.HistorySyncNotification, int, error) {
|
||||
var notifBytes []byte
|
||||
var rowid int
|
||||
err := hsnq.QueryRow(ctx, getNextHSNotificationQuery, hsnq.BridgeID, loginID).Scan(&rowid, ¬ifBytes)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, 0, nil
|
||||
} else if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
var notif waE2E.HistorySyncNotification
|
||||
if err = proto.Unmarshal(notifBytes, ¬if); err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
return ¬if, rowid, nil
|
||||
}
|
||||
|
||||
func (hsnq *HistorySyncNotificationQuery) Delete(ctx context.Context, rowid int) error {
|
||||
_, err := hsnq.Exec(ctx, deleteHSNotificationQuery, rowid)
|
||||
return err
|
||||
}
|
||||
|
|
@ -116,12 +116,9 @@ func (mq *MessageQuery) GetBetween(ctx context.Context, loginID networkid.UserLo
|
|||
AsList()
|
||||
}
|
||||
|
||||
func (mq *MessageQuery) DeleteBetween(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID, before, after uint64) (int64, error) {
|
||||
res, err := mq.Exec(ctx, deleteHistorySyncMessagesBetweenQuery, mq.BridgeID, loginID, chatJID, before, after)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.RowsAffected()
|
||||
func (mq *MessageQuery) DeleteBetween(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID, before, after uint64) error {
|
||||
_, err := mq.Exec(ctx, deleteHistorySyncMessagesBetweenQuery, mq.BridgeID, loginID, chatJID, before, after)
|
||||
return err
|
||||
}
|
||||
|
||||
func (mq *MessageQuery) DeleteAll(ctx context.Context, loginID networkid.UserLoginID) error {
|
||||
|
|
@ -129,12 +126,9 @@ func (mq *MessageQuery) DeleteAll(ctx context.Context, loginID networkid.UserLog
|
|||
return err
|
||||
}
|
||||
|
||||
func (mq *MessageQuery) DeleteAllInChat(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID) (int64, error) {
|
||||
res, err := mq.Exec(ctx, deleteHistorySyncMessagesForPortalQuery, mq.BridgeID, loginID, chatJID)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.RowsAffected()
|
||||
func (mq *MessageQuery) DeleteAllInChat(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID) error {
|
||||
_, err := mq.Exec(ctx, deleteHistorySyncMessagesForPortalQuery, mq.BridgeID, loginID, chatJID)
|
||||
return err
|
||||
}
|
||||
|
||||
func (mq *MessageQuery) ConversationHasMessages(ctx context.Context, loginID networkid.UserLoginID, chatJID types.JID) (exists bool, err error) {
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
-- v0 -> v9 (compatible with v3+): Latest revision
|
||||
-- v0 -> v4 (compatible with v3+): Latest revision
|
||||
|
||||
CREATE TABLE whatsapp_poll_option_id (
|
||||
bridge_id TEXT NOT NULL,
|
||||
|
|
@ -26,7 +26,8 @@ CREATE TABLE whatsapp_history_sync_conversation (
|
|||
ephemeral_setting_timestamp BIGINT,
|
||||
marked_as_unread BOOLEAN,
|
||||
unread_count INTEGER,
|
||||
synced_login_ts BIGINT,
|
||||
|
||||
bridged BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
PRIMARY KEY (bridge_id, user_login_id, chat_jid),
|
||||
CONSTRAINT whatsapp_history_sync_conversation_user_login_fkey FOREIGN KEY (bridge_id, user_login_id)
|
||||
|
|
@ -73,28 +74,3 @@ CREATE TABLE whatsapp_media_backfill_request (
|
|||
);
|
||||
CREATE INDEX whatsapp_media_backfill_request_portal_idx ON whatsapp_media_backfill_request (bridge_id, portal_id, portal_receiver);
|
||||
CREATE INDEX whatsapp_media_backfill_request_message_idx ON whatsapp_media_backfill_request (bridge_id, portal_receiver, message_id, _part_id);
|
||||
|
||||
CREATE TABLE whatsapp_history_sync_notification (
|
||||
-- only: sqlite (line commented)
|
||||
-- rowid INTEGER PRIMARY KEY,
|
||||
-- only: postgres
|
||||
rowid BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
|
||||
|
||||
bridge_id TEXT NOT NULL,
|
||||
user_login_id TEXT NOT NULL,
|
||||
data bytea NOT NULL,
|
||||
|
||||
CONSTRAINT whatsapp_history_sync_notification_user_login_fkey FOREIGN KEY (bridge_id, user_login_id)
|
||||
REFERENCES user_login (bridge_id, id) ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX whatsapp_history_sync_notification_login_idx ON whatsapp_history_sync_notification (bridge_id, user_login_id);
|
||||
|
||||
CREATE TABLE whatsapp_avatar_cache (
|
||||
entity_jid TEXT NOT NULL,
|
||||
avatar_id TEXT NOT NULL,
|
||||
direct_path TEXT NOT NULL,
|
||||
expiry BIGINT NOT NULL,
|
||||
gone BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
PRIMARY KEY (entity_jid, avatar_id)
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,15 +0,0 @@
|
|||
-- v5 (compatible with v3+): Add buffer for history sync notifications
|
||||
CREATE TABLE whatsapp_history_sync_notification (
|
||||
-- only: sqlite (line commented)
|
||||
-- rowid INTEGER PRIMARY KEY,
|
||||
-- only: postgres
|
||||
rowid BIGINT PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
|
||||
|
||||
bridge_id TEXT NOT NULL,
|
||||
user_login_id TEXT NOT NULL,
|
||||
data bytea NOT NULL,
|
||||
|
||||
CONSTRAINT whatsapp_history_sync_notification_user_login_fkey FOREIGN KEY (bridge_id, user_login_id)
|
||||
REFERENCES user_login (bridge_id, id) ON UPDATE CASCADE ON DELETE CASCADE
|
||||
);
|
||||
CREATE INDEX whatsapp_history_sync_notification_login_idx ON whatsapp_history_sync_notification (bridge_id, user_login_id);
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
-- v6 (compatible with v3+): Store timestamp for which login a conversation was synced with
|
||||
ALTER TABLE whatsapp_history_sync_conversation ADD COLUMN synced_login_ts BIGINT;
|
||||
UPDATE whatsapp_history_sync_conversation SET synced_login_ts = 0 WHERE bridged = true;
|
||||
ALTER TABLE whatsapp_history_sync_conversation DROP COLUMN bridged;
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
-- v7 (compatible with v3+): Add cache for avatar URLs when using direct media
|
||||
CREATE TABLE whatsapp_avatar_cache (
|
||||
entity_jid TEXT NOT NULL,
|
||||
avatar_id TEXT NOT NULL,
|
||||
direct_path TEXT NOT NULL,
|
||||
expiry BIGINT NOT NULL,
|
||||
gone BOOLEAN NOT NULL DEFAULT false,
|
||||
|
||||
PRIMARY KEY (entity_jid, avatar_id)
|
||||
);
|
||||
|
|
@ -1,2 +0,0 @@
|
|||
-- v8 (compatible with v3+): Mark LID DMs for deletion
|
||||
INSERT INTO kv_store (bridge_id, key, value) VALUES ('', 'whatsapp_lid_dms_deleted', 'false');
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
-- v9 (compatible with v3+): Mark LID DMs for deletion (again)
|
||||
DELETE FROM kv_store WHERE bridge_id='' AND key='whatsapp_lid_dms_deleted';
|
||||
INSERT INTO kv_store (bridge_id, key, value) VALUES ('', 'whatsapp_lid_dms_deleted', 'false');
|
||||
|
|
@ -124,6 +124,10 @@ func getMessageType(waMsg *waE2E.Message) string {
|
|||
case waMsg.EncEventResponseMessage != nil:
|
||||
return "ignore" // these are ignored for now as they're not meant to be shown as new messages
|
||||
//return "encrypted event response"
|
||||
case waMsg.CommentMessage != nil:
|
||||
return "comment"
|
||||
case waMsg.EncCommentMessage != nil:
|
||||
return "encrypted comment"
|
||||
case waMsg.NewsletterAdminInviteMessage != nil:
|
||||
return "newsletter admin invite"
|
||||
case waMsg.SecretEncryptedMessage != nil:
|
||||
|
|
|
|||
|
|
@ -19,18 +19,15 @@ package msgconv
|
|||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/jpeg"
|
||||
"image/png"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/ffmpeg"
|
||||
|
|
@ -40,6 +37,7 @@ import (
|
|||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/proto/waE2E"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"golang.org/x/image/webp"
|
||||
"google.golang.org/protobuf/proto"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/database"
|
||||
|
|
@ -50,13 +48,7 @@ import (
|
|||
"go.mau.fi/mautrix-whatsapp/pkg/waid"
|
||||
)
|
||||
|
||||
func (mc *MessageConverter) generateContextInfo(
|
||||
ctx context.Context,
|
||||
replyTo *database.Message,
|
||||
portal *bridgev2.Portal,
|
||||
perMessageTimer *event.BeeperDisappearingTimer,
|
||||
roomMention bool,
|
||||
) *waE2E.ContextInfo {
|
||||
func (mc *MessageConverter) generateContextInfo(replyTo *database.Message, portal *bridgev2.Portal) (*waE2E.ContextInfo, error) {
|
||||
contextInfo := &waE2E.ContextInfo{}
|
||||
if replyTo != nil {
|
||||
msgID, err := waid.ParseMessageID(replyTo.ID)
|
||||
|
|
@ -64,31 +56,18 @@ func (mc *MessageConverter) generateContextInfo(
|
|||
contextInfo.StanzaID = proto.String(msgID.ID)
|
||||
contextInfo.Participant = proto.String(msgID.Sender.String())
|
||||
contextInfo.QuotedMessage = &waE2E.Message{Conversation: proto.String("")}
|
||||
contextInfo.QuotedType = waE2E.ContextInfo_EXPLICIT.Enum()
|
||||
} else {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).
|
||||
Stringer("reply_to_event_id", replyTo.MXID).
|
||||
Str("reply_to_message_id", string(replyTo.ID)).
|
||||
Msg("Failed to parse reply to message ID")
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var timer time.Duration
|
||||
if perMessageTimer != nil {
|
||||
timer = perMessageTimer.Timer.Duration
|
||||
} else {
|
||||
timer = portal.Disappear.Timer
|
||||
}
|
||||
if timer > 0 {
|
||||
contextInfo.Expiration = ptr.Ptr(uint32(timer.Seconds()))
|
||||
}
|
||||
if portal.Disappear.Timer > 0 {
|
||||
contextInfo.Expiration = ptr.Ptr(uint32(portal.Disappear.Timer.Seconds()))
|
||||
setAt := portal.Metadata.(*waid.PortalMetadata).DisappearingTimerSetAt
|
||||
if setAt > 0 && contextInfo.Expiration != nil {
|
||||
if setAt > 0 {
|
||||
contextInfo.EphemeralSettingTimestamp = ptr.Ptr(setAt)
|
||||
}
|
||||
if roomMention {
|
||||
contextInfo.NonJIDMentions = proto.Uint32(1)
|
||||
}
|
||||
return contextInfo
|
||||
return contextInfo, nil
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) ToWhatsApp(
|
||||
|
|
@ -107,15 +86,14 @@ func (mc *MessageConverter) ToWhatsApp(
|
|||
}
|
||||
|
||||
message := &waE2E.Message{}
|
||||
contextInfo := mc.generateContextInfo(ctx, replyTo, portal, content.BeeperDisappearingTimer, content.Mentions != nil && content.Mentions.Room)
|
||||
|
||||
switch content.MsgType {
|
||||
case event.MsgText, event.MsgNotice, event.MsgEmote:
|
||||
var err error
|
||||
message, err = mc.constructTextMessage(ctx, content, evt.Content.Raw, contextInfo)
|
||||
contextInfo, err := mc.generateContextInfo(replyTo, portal)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
switch content.MsgType {
|
||||
case event.MsgText, event.MsgNotice, event.MsgEmote:
|
||||
message = mc.constructTextMessage(ctx, content, contextInfo)
|
||||
case event.MessageType(event.EventSticker.Type), event.MsgImage, event.MsgVideo, event.MsgAudio, event.MsgFile:
|
||||
uploaded, thumbnail, mime, err := mc.reuploadFileToWhatsApp(ctx, content)
|
||||
if err != nil {
|
||||
|
|
@ -144,7 +122,7 @@ func (mc *MessageConverter) ToWhatsApp(
|
|||
return nil, nil, fmt.Errorf("failed to parse message ID: %w", err)
|
||||
}
|
||||
rootMsgInfo := MessageIDToInfo(client, parsedID)
|
||||
message, err = client.EncryptComment(ctx, rootMsgInfo, message)
|
||||
message, err = client.EncryptComment(rootMsgInfo, message)
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to encrypt comment: %w", err)
|
||||
}
|
||||
|
|
@ -202,7 +180,6 @@ func (mc *MessageConverter) constructMediaMessage(
|
|||
FileSHA256: uploaded.FileSHA256,
|
||||
FileLength: proto.Uint64(uploaded.FileLength),
|
||||
URL: proto.String(uploaded.URL),
|
||||
IsLottie: proto.Bool(mime == "application/was"),
|
||||
},
|
||||
}
|
||||
case event.MsgAudio:
|
||||
|
|
@ -272,14 +249,9 @@ func (mc *MessageConverter) constructMediaMessage(
|
|||
},
|
||||
}
|
||||
case event.MsgFile:
|
||||
fileName := content.FileName
|
||||
if fileName == "" {
|
||||
fileName = content.Body
|
||||
}
|
||||
|
||||
msg := &waE2E.Message{
|
||||
DocumentMessage: &waE2E.DocumentMessage{
|
||||
FileName: proto.String(fileName),
|
||||
FileName: proto.String(content.FileName),
|
||||
|
||||
Caption: proto.String(caption),
|
||||
JPEGThumbnail: thumbnail,
|
||||
|
|
@ -321,16 +293,7 @@ func (mc *MessageConverter) parseText(ctx context.Context, content *event.Messag
|
|||
return
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) constructTextMessage(
|
||||
ctx context.Context,
|
||||
content *event.MessageEventContent,
|
||||
raw map[string]any,
|
||||
contextInfo *waE2E.ContextInfo,
|
||||
) (*waE2E.Message, error) {
|
||||
groupInvite, ok := raw[GroupInviteMetaField].(map[string]any)
|
||||
if ok {
|
||||
return mc.constructGroupInviteMessage(ctx, content, groupInvite, contextInfo)
|
||||
}
|
||||
func (mc *MessageConverter) constructTextMessage(ctx context.Context, content *event.MessageEventContent, contextInfo *waE2E.ContextInfo) *waE2E.Message {
|
||||
text, mentions := mc.parseText(ctx, content)
|
||||
if len(mentions) > 0 {
|
||||
contextInfo.MentionedJID = mentions
|
||||
|
|
@ -341,44 +304,7 @@ func (mc *MessageConverter) constructTextMessage(
|
|||
}
|
||||
mc.convertURLPreviewToWhatsApp(ctx, content, etm)
|
||||
|
||||
return &waE2E.Message{ExtendedTextMessage: etm}, nil
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) constructGroupInviteMessage(
|
||||
ctx context.Context,
|
||||
content *event.MessageEventContent,
|
||||
inviteMeta map[string]any,
|
||||
contextInfo *waE2E.ContextInfo,
|
||||
) (*waE2E.Message, error) {
|
||||
payload, err := json.Marshal(inviteMeta)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to marshal invite meta: %w", err)
|
||||
}
|
||||
var parsedInviteMeta waid.GroupInviteMeta
|
||||
err = json.Unmarshal(payload, &parsedInviteMeta)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse invite meta: %w", err)
|
||||
}
|
||||
text, mentions := mc.parseText(ctx, content)
|
||||
if len(mentions) > 0 {
|
||||
contextInfo.MentionedJID = mentions
|
||||
}
|
||||
groupType := waE2E.GroupInviteMessage_DEFAULT
|
||||
if parsedInviteMeta.IsParentGroup {
|
||||
groupType = waE2E.GroupInviteMessage_PARENT
|
||||
}
|
||||
return &waE2E.Message{
|
||||
GroupInviteMessage: &waE2E.GroupInviteMessage{
|
||||
GroupJID: proto.String(parsedInviteMeta.JID.String()),
|
||||
InviteCode: proto.String(parsedInviteMeta.Code),
|
||||
InviteExpiration: proto.Int64(parsedInviteMeta.Expiration),
|
||||
GroupName: proto.String(parsedInviteMeta.GroupName),
|
||||
JPEGThumbnail: nil,
|
||||
Caption: proto.String(text),
|
||||
ContextInfo: contextInfo,
|
||||
GroupType: groupType.Enum(),
|
||||
},
|
||||
}, nil
|
||||
return &waE2E.Message{ExtendedTextMessage: etm}
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) convertPill(displayname, mxid, eventID string, ctx format.Context) string {
|
||||
|
|
@ -434,18 +360,18 @@ func (img *PaddedImage) At(x, y int) color.Color {
|
|||
return img.Image.At(x-img.OffsetX, y-img.OffsetY)
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) convertToJPEG(webpImage []byte) ([]byte, error) {
|
||||
decoded, _, err := image.Decode(bytes.NewReader(webpImage))
|
||||
func (mc *MessageConverter) convertWebPtoPNG(webpImage []byte) ([]byte, error) {
|
||||
webpDecoded, err := webp.Decode(bytes.NewReader(webpImage))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode webp image: %w", err)
|
||||
}
|
||||
|
||||
var jpgBuffer bytes.Buffer
|
||||
if err = jpeg.Encode(&jpgBuffer, decoded, &jpeg.Options{Quality: 80}); err != nil {
|
||||
var pngBuffer bytes.Buffer
|
||||
if err = png.Encode(&pngBuffer, webpDecoded); err != nil {
|
||||
return nil, fmt.Errorf("failed to encode png image: %w", err)
|
||||
}
|
||||
|
||||
return jpgBuffer.Bytes(), nil
|
||||
return pngBuffer.Bytes(), nil
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) convertToWebP(img []byte) ([]byte, int, error) {
|
||||
|
|
@ -484,17 +410,6 @@ func (mc *MessageConverter) convertToWebP(img []byte) ([]byte, int, error) {
|
|||
return webpBuffer.Bytes(), size, nil
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) getOriginalBridgedSticker(ctx context.Context, info *event.BridgedSticker) (*types.StickerPackItem, error) {
|
||||
if info == nil || info.Network != StickerSourceID || !strings.HasPrefix(info.PackURL, StickerPackURLPrefix) || info.ID == "" {
|
||||
return nil, nil
|
||||
}
|
||||
fileHash, err := base64.StdEncoding.DecodeString(info.ID)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
return mc.GetCachedSticker(ctx, getClient(ctx), strings.TrimPrefix(info.PackURL, StickerPackURLPrefix), fileHash)
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) reuploadFileToWhatsApp(
|
||||
ctx context.Context, content *event.MessageEventContent,
|
||||
) (*whatsmeow.UploadResponse, []byte, string, error) {
|
||||
|
|
@ -503,25 +418,7 @@ func (mc *MessageConverter) reuploadFileToWhatsApp(
|
|||
if content.FileName != "" {
|
||||
fileName = content.FileName
|
||||
}
|
||||
var data []byte
|
||||
var err error
|
||||
var sticker *types.StickerPackItem
|
||||
if sticker, err = mc.getOriginalBridgedSticker(ctx, content.Info.BridgedSticker); err != nil {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).
|
||||
Msg("Failed to get original bridged sticker, falling back to downloading from URL")
|
||||
data, err = mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
|
||||
} else if sticker != nil {
|
||||
if sticker.MimeType == "application/was" {
|
||||
data, err = getClient(ctx).Download(ctx, sticker)
|
||||
mime = sticker.MimeType
|
||||
} else {
|
||||
data, err = mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
|
||||
}
|
||||
content.Info.Width = sticker.Width
|
||||
content.Info.Height = sticker.Height
|
||||
} else {
|
||||
data, err = mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
|
||||
}
|
||||
data, err := mc.Bridge.Bot.DownloadMedia(ctx, content.URL, content.File)
|
||||
if err != nil {
|
||||
return nil, nil, "", fmt.Errorf("%w: %w", bridgev2.ErrMediaDownloadFailed, err)
|
||||
}
|
||||
|
|
@ -539,39 +436,27 @@ func (mc *MessageConverter) reuploadFileToWhatsApp(
|
|||
case event.MessageType(event.EventSticker.Type):
|
||||
isSticker = true
|
||||
mediaType = whatsmeow.MediaImage
|
||||
if mime == "video/lottie+json" {
|
||||
// This likely won't work
|
||||
data, err = PackAnimatedSticker(data)
|
||||
if err != nil {
|
||||
return nil, nil, mime, fmt.Errorf("%w (packing animated sticker): %w", bridgev2.ErrMediaConvertFailed, err)
|
||||
}
|
||||
mime = "application/was"
|
||||
} else if (mime != "image/webp" || content.Info.Width != content.Info.Height) && mime != "application/was" {
|
||||
if mime != "image/webp" || content.Info.Width != content.Info.Height {
|
||||
var size int
|
||||
data, size, err = mc.convertToWebP(data)
|
||||
if err != nil {
|
||||
if mime != "image/webp" {
|
||||
return nil, nil, "image/webp", fmt.Errorf("%w (to webp): %w", bridgev2.ErrMediaConvertFailed, err)
|
||||
} else {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Failed to add padding to webp, continuing with original file")
|
||||
}
|
||||
} else {
|
||||
content.Info.Width = size
|
||||
content.Info.Height = size
|
||||
mime = "image/webp"
|
||||
}
|
||||
}
|
||||
case event.MsgImage:
|
||||
mediaType = whatsmeow.MediaImage
|
||||
switch mime {
|
||||
case "image/jpeg":
|
||||
case "image/jpeg", "image/png":
|
||||
// allowed
|
||||
case "image/webp", "image/png":
|
||||
data, err = mc.convertToJPEG(data)
|
||||
case "image/webp":
|
||||
data, err = mc.convertWebPtoPNG(data)
|
||||
if err != nil {
|
||||
return nil, nil, "image/webp", fmt.Errorf("%w (webp to png): %s", bridgev2.ErrMediaConvertFailed, err)
|
||||
}
|
||||
mime = "image/jpeg"
|
||||
mime = "image/png"
|
||||
default:
|
||||
return nil, nil, mime, fmt.Errorf("%w %s in image message", bridgev2.ErrUnsupportedMediaType, mime)
|
||||
}
|
||||
|
|
@ -579,17 +464,13 @@ func (mc *MessageConverter) reuploadFileToWhatsApp(
|
|||
switch mime {
|
||||
case "video/mp4", "video/3gpp":
|
||||
// allowed
|
||||
case "video/webm", "video/quicktime":
|
||||
sourceFormat := "webm"
|
||||
if mime == "video/quicktime" {
|
||||
sourceFormat = "mov"
|
||||
}
|
||||
data, err = ffmpeg.ConvertBytes(ctx, data, ".mp4", []string{"-f", sourceFormat}, []string{
|
||||
case "video/webm":
|
||||
data, err = ffmpeg.ConvertBytes(ctx, data, ".mp4", []string{"-f", "webm"}, []string{
|
||||
"-pix_fmt", "yuv420p", "-c:v", "libx264",
|
||||
"-filter:v", "crop='floor(in_w/2)*2:floor(in_h/2)*2'",
|
||||
}, mime)
|
||||
if err != nil {
|
||||
return nil, nil, mime, fmt.Errorf("%w (%s to mp4): %w", bridgev2.ErrMediaConvertFailed, sourceFormat, err)
|
||||
return nil, nil, "video/webm", fmt.Errorf("%w (webm to mp4): %w", bridgev2.ErrMediaConvertFailed, err)
|
||||
}
|
||||
mime = "video/mp4"
|
||||
case "image/gif":
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ import (
|
|||
"go.mau.fi/whatsmeow/types"
|
||||
_ "golang.org/x/image/webp"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/database"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
|
@ -46,7 +47,6 @@ const (
|
|||
contextKeyClient contextKey = iota
|
||||
contextKeyIntent
|
||||
contextKeyPortal
|
||||
ContextKeyEditTargetID
|
||||
)
|
||||
|
||||
func getClient(ctx context.Context) *whatsmeow.Client {
|
||||
|
|
@ -61,39 +61,15 @@ func getPortal(ctx context.Context) *bridgev2.Portal {
|
|||
return ctx.Value(contextKeyPortal).(*bridgev2.Portal)
|
||||
}
|
||||
|
||||
func getEditTargetID(ctx context.Context) types.MessageID {
|
||||
editID, _ := ctx.Value(ContextKeyEditTargetID).(types.MessageID)
|
||||
return editID
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) getBasicUserInfo(ctx context.Context, user types.JID) (id.UserID, string, error) {
|
||||
ghost, err := mc.Bridge.GetGhostByID(ctx, waid.MakeUserID(user))
|
||||
func (mc *MessageConverter) getBasicUserInfo(ctx context.Context, user networkid.UserID) (id.UserID, string, error) {
|
||||
ghost, err := mc.Bridge.GetGhostByID(ctx, user)
|
||||
if err != nil {
|
||||
return "", "", fmt.Errorf("failed to get ghost by ID: %w", err)
|
||||
}
|
||||
var pnJID types.JID
|
||||
if user.Server == types.DefaultUserServer {
|
||||
pnJID = user
|
||||
} else if user.Server == types.HiddenUserServer {
|
||||
cli := getClient(ctx)
|
||||
if user.User == cli.Store.GetLID().User {
|
||||
pnJID = cli.Store.GetJID()
|
||||
} else {
|
||||
pnJID, err = cli.Store.LIDs.GetPNForLID(ctx, user)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).
|
||||
Stringer("lid", user).
|
||||
Msg("Failed to get PN for LID in mention bridging")
|
||||
}
|
||||
}
|
||||
}
|
||||
if !pnJID.IsEmpty() {
|
||||
portal := getPortal(ctx)
|
||||
login := mc.Bridge.GetCachedUserLoginByID(waid.MakeUserLoginID(pnJID))
|
||||
if login != nil && (portal.Receiver == "" || portal.Receiver == login.ID) {
|
||||
login := mc.Bridge.GetCachedUserLoginByID(networkid.UserLoginID(user))
|
||||
if login != nil {
|
||||
return login.UserMXID, ghost.Name, nil
|
||||
}
|
||||
}
|
||||
return ghost.Intent.GetMXID(), ghost.Name, nil
|
||||
}
|
||||
|
||||
|
|
@ -108,7 +84,7 @@ func (mc *MessageConverter) addMentions(ctx context.Context, mentionedJID []stri
|
|||
zerolog.Ctx(ctx).Err(err).Str("jid", jid).Msg("Failed to parse mentioned JID")
|
||||
continue
|
||||
}
|
||||
mxid, displayname, err := mc.getBasicUserInfo(ctx, parsed)
|
||||
mxid, displayname, err := mc.getBasicUserInfo(ctx, waid.MakeUserID(parsed))
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Str("jid", jid).Msg("Failed to get user info")
|
||||
continue
|
||||
|
|
@ -134,15 +110,10 @@ func (mc *MessageConverter) ToMatrix(
|
|||
client *whatsmeow.Client,
|
||||
intent bridgev2.MatrixAPI,
|
||||
waMsg *waE2E.Message,
|
||||
rawWaMsg *waE2E.Message,
|
||||
info *types.MessageInfo,
|
||||
isViewOnce bool,
|
||||
isBackfill bool,
|
||||
previouslyConvertedPart *bridgev2.ConvertedMessagePart,
|
||||
) *bridgev2.ConvertedMessage {
|
||||
if waMsg == nil {
|
||||
waMsg = &waE2E.Message{}
|
||||
}
|
||||
ctx = context.WithValue(ctx, contextKeyClient, client)
|
||||
ctx = context.WithValue(ctx, contextKeyIntent, intent)
|
||||
ctx = context.WithValue(ctx, contextKeyPortal, portal)
|
||||
|
|
@ -179,12 +150,6 @@ func (mc *MessageConverter) ToMatrix(
|
|||
part, contextInfo = mc.convertPollUpdateMessage(ctx, info, waMsg.PollUpdateMessage)
|
||||
case waMsg.EventMessage != nil:
|
||||
part, contextInfo = mc.convertEventMessage(ctx, waMsg.EventMessage)
|
||||
case waMsg.PinInChatMessage != nil:
|
||||
part, contextInfo = mc.convertPinInChatMessage(ctx, waMsg.PinInChatMessage)
|
||||
case waMsg.KeepInChatMessage != nil:
|
||||
part, contextInfo = mc.convertKeepInChatMessage(ctx, waMsg.KeepInChatMessage)
|
||||
case waMsg.RichResponseMessage != nil:
|
||||
part, contextInfo = mc.convertRichResponseMessage(ctx, waMsg.RichResponseMessage)
|
||||
case waMsg.ImageMessage != nil:
|
||||
part, contextInfo = mc.convertMediaMessage(ctx, waMsg.ImageMessage, "photo", info, isViewOnce, previouslyConvertedPart)
|
||||
case waMsg.StickerMessage != nil:
|
||||
|
|
@ -201,8 +166,6 @@ func (mc *MessageConverter) ToMatrix(
|
|||
part, contextInfo = mc.convertMediaMessage(ctx, waMsg.AudioMessage, typeName, info, isViewOnce, previouslyConvertedPart)
|
||||
case waMsg.DocumentMessage != nil:
|
||||
part, contextInfo = mc.convertMediaMessage(ctx, waMsg.DocumentMessage, "file attachment", info, isViewOnce, previouslyConvertedPart)
|
||||
case waMsg.AlbumMessage != nil:
|
||||
part, contextInfo = mc.convertAlbumMessage(ctx, waMsg.AlbumMessage)
|
||||
case waMsg.LocationMessage != nil:
|
||||
part, contextInfo = mc.convertLocationMessage(ctx, waMsg.LocationMessage)
|
||||
case waMsg.LiveLocationMessage != nil:
|
||||
|
|
@ -216,11 +179,11 @@ func (mc *MessageConverter) ToMatrix(
|
|||
case waMsg.GroupInviteMessage != nil:
|
||||
part, contextInfo = mc.convertGroupInviteMessage(ctx, info, waMsg.GroupInviteMessage)
|
||||
case waMsg.ProtocolMessage != nil && waMsg.ProtocolMessage.GetType() == waE2E.ProtocolMessage_EPHEMERAL_SETTING:
|
||||
part, contextInfo = mc.convertEphemeralSettingMessage(ctx, waMsg.ProtocolMessage, info.Timestamp, isBackfill)
|
||||
part, contextInfo = mc.convertEphemeralSettingMessage(ctx, waMsg.ProtocolMessage)
|
||||
case waMsg.EncCommentMessage != nil:
|
||||
part = failedCommentPart
|
||||
default:
|
||||
part, contextInfo = mc.convertUnknownMessage(ctx, rawWaMsg)
|
||||
part, contextInfo = mc.convertUnknownMessage(ctx, waMsg)
|
||||
}
|
||||
|
||||
part.Content.Mentions = &event.Mentions{}
|
||||
|
|
@ -237,25 +200,16 @@ func (mc *MessageConverter) ToMatrix(
|
|||
part.Extra["fi.mau.whatsapp.source_broadcast_list"] = info.Chat.String()
|
||||
}
|
||||
mc.addMentions(ctx, contextInfo.GetMentionedJID(), part.Content)
|
||||
if contextInfo.GetNonJIDMentions() == 1 {
|
||||
part.Content.Mentions.Room = true
|
||||
}
|
||||
|
||||
cm := &bridgev2.ConvertedMessage{
|
||||
Parts: []*bridgev2.ConvertedMessagePart{part},
|
||||
}
|
||||
if contextInfo.GetExpiration() > 0 {
|
||||
cm.Disappear.Timer = time.Duration(contextInfo.GetExpiration()) * time.Second
|
||||
cm.Disappear.Type = event.DisappearingTypeAfterSend
|
||||
}
|
||||
cm.Disappear.Type = database.DisappearingTypeAfterRead
|
||||
if portal.Disappear.Timer != cm.Disappear.Timer && portal.Metadata.(*waid.PortalMetadata).DisappearingTimerSetAt < contextInfo.GetEphemeralSettingTimestamp() {
|
||||
portal.UpdateDisappearingSetting(ctx, cm.Disappear, bridgev2.UpdateDisappearingSettingOpts{
|
||||
Sender: intent,
|
||||
Timestamp: info.Timestamp,
|
||||
Implicit: true,
|
||||
Save: true,
|
||||
SendNotice: true,
|
||||
})
|
||||
portal.UpdateDisappearingSetting(ctx, cm.Disappear, intent, info.Timestamp, true, true)
|
||||
}
|
||||
}
|
||||
if contextInfo.GetStanzaID() != "" {
|
||||
pcp, _ := types.ParseJID(contextInfo.GetParticipant())
|
||||
|
|
@ -263,46 +217,10 @@ func (mc *MessageConverter) ToMatrix(
|
|||
if chat.IsEmpty() {
|
||||
chat, _ = waid.ParsePortalID(portal.ID)
|
||||
}
|
||||
// We reroute all DMs to the phone number JID, so reroute reply participants too
|
||||
pcp = rerouteMessageKey(ctx, chat, pcp, getPortal(ctx).Metadata.(*waid.PortalMetadata).AddressingMode == types.AddressingModeLID)
|
||||
if store := getClient(ctx).Store; store != nil && chat.Server == types.DefaultUserServer && pcp.Server == types.HiddenUserServer {
|
||||
pcpPN, _ := store.LIDs.GetPNForLID(ctx, pcp)
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Stringer("orig_participant", pcp).
|
||||
Stringer("rerouted_participant", pcpPN).
|
||||
Msg("Rerouting reply target (PN recipient in LID DM)")
|
||||
if !pcpPN.IsEmpty() {
|
||||
pcp = pcpPN
|
||||
}
|
||||
} else if store != nil && chat.Server == types.GroupServer && pcp.Server == types.DefaultUserServer && getPortal(ctx).Metadata.(*waid.PortalMetadata).AddressingMode == types.AddressingModeLID {
|
||||
pcpLID, _ := store.LIDs.GetLIDForPN(ctx, pcp)
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Stringer("orig_participant", pcp).
|
||||
Stringer("rerouted_participant", pcpLID).
|
||||
Msg("Rerouting reply target (PN recipient in LID group)")
|
||||
if !pcpLID.IsEmpty() {
|
||||
pcp = pcpLID
|
||||
}
|
||||
}
|
||||
cm.ReplyTo = &networkid.MessageOptionalPartID{
|
||||
MessageID: waid.MakeMessageID(chat, pcp, contextInfo.GetStanzaID()),
|
||||
}
|
||||
}
|
||||
if contextInfo.GetIsForwarded() {
|
||||
hasCaption := part.Content.FileName != "" && part.Content.FileName != part.Content.Body
|
||||
isMedia := part.Content.MsgType.IsMedia()
|
||||
isText := part.Content.MsgType.IsText()
|
||||
if isMedia && !hasCaption {
|
||||
part.Content.FileName = part.Content.Body
|
||||
part.Content.Body = "↷ Forwarded"
|
||||
part.Content.Format = event.FormatHTML
|
||||
part.Content.FormattedBody = "<p data-mx-forwarded-notice><em>↷ Forwarded</em></p>"
|
||||
} else if isText || isMedia {
|
||||
part.Content.EnsureHasHTML()
|
||||
part.Content.Body = "↷ Forwarded\n\n" + part.Content.Body
|
||||
part.Content.FormattedBody = "<p data-mx-forwarded-notice><em>↷ Forwarded</em></p>" + part.Content.FormattedBody
|
||||
}
|
||||
}
|
||||
commentTarget := waMsg.GetEncCommentMessage().GetTargetMessageKey()
|
||||
if commentTarget == nil {
|
||||
commentTarget = waMsg.GetCommentMessage().GetTargetMessageKey()
|
||||
|
|
|
|||
|
|
@ -71,7 +71,10 @@ func (mc *MessageConverter) PollStartToWhatsApp(
|
|||
if maxAnswers >= len(content.PollStart.Answers) || maxAnswers < 0 {
|
||||
maxAnswers = 0
|
||||
}
|
||||
contextInfo := mc.generateContextInfo(ctx, replyTo, portal, nil, content.Mentions != nil && content.Mentions.Room)
|
||||
contextInfo, err := mc.generateContextInfo(replyTo, portal)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
var question string
|
||||
question, contextInfo.MentionedJID = mc.msc1767ToWhatsApp(ctx, content.PollStart.Question, content.Mentions)
|
||||
if len(question) == 0 {
|
||||
|
|
@ -140,7 +143,7 @@ func (mc *MessageConverter) PollVoteToWhatsApp(
|
|||
}
|
||||
}
|
||||
}
|
||||
pollUpdate, err := client.EncryptPollVote(ctx, pollMsgInfo, &waE2E.PollVoteMessage{
|
||||
pollUpdate, err := client.EncryptPollVote(pollMsgInfo, &waE2E.PollVoteMessage{
|
||||
SelectedOptions: optionHashes,
|
||||
})
|
||||
return &waE2E.Message{PollUpdateMessage: pollUpdate}, err
|
||||
|
|
|
|||
|
|
@ -17,9 +17,6 @@
|
|||
package msgconv
|
||||
|
||||
import (
|
||||
"sync"
|
||||
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/format"
|
||||
|
||||
|
|
@ -46,16 +43,12 @@ type MessageConverter struct {
|
|||
DisableViewOnce bool
|
||||
DirectMedia bool
|
||||
OldMediaSuffix string
|
||||
|
||||
stickerPackCache map[string]*types.StickerPack
|
||||
stickerPackCacheLock sync.Mutex
|
||||
}
|
||||
|
||||
func New(br *bridgev2.Bridge) *MessageConverter {
|
||||
mc := &MessageConverter{
|
||||
Bridge: br,
|
||||
MaxFileSize: 50 * 1024 * 1024,
|
||||
stickerPackCache: make(map[string]*types.StickerPack),
|
||||
}
|
||||
mc.HTMLParser = &format.HTMLParser{
|
||||
PillConverter: mc.convertPill,
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ func (mc *MessageConverter) convertURLPreviewToBeeper(ctx context.Context, msg *
|
|||
var thumbnailData []byte
|
||||
if msg.ThumbnailDirectPath != nil {
|
||||
var err error
|
||||
thumbnailData, err = getClient(ctx).DownloadThumbnail(ctx, msg)
|
||||
thumbnailData, err = getClient(ctx).DownloadThumbnail(msg)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to download thumbnail for link preview")
|
||||
}
|
||||
|
|
@ -60,16 +60,16 @@ func (mc *MessageConverter) convertURLPreviewToBeeper(ctx context.Context, msg *
|
|||
thumbnailData = msg.JPEGThumbnail
|
||||
}
|
||||
if thumbnailData != nil {
|
||||
output.ImageHeight = event.IntOrString(msg.GetThumbnailHeight())
|
||||
output.ImageWidth = event.IntOrString(msg.GetThumbnailWidth())
|
||||
output.ImageHeight = int(msg.GetThumbnailHeight())
|
||||
output.ImageWidth = int(msg.GetThumbnailWidth())
|
||||
if output.ImageHeight == 0 || output.ImageWidth == 0 {
|
||||
src, _, err := image.Decode(bytes.NewReader(thumbnailData))
|
||||
if err == nil {
|
||||
imageBounds := src.Bounds()
|
||||
output.ImageWidth, output.ImageHeight = event.IntOrString(imageBounds.Max.X), event.IntOrString(imageBounds.Max.Y)
|
||||
output.ImageWidth, output.ImageHeight = imageBounds.Max.X, imageBounds.Max.Y
|
||||
}
|
||||
}
|
||||
output.ImageSize = event.IntOrString(len(thumbnailData))
|
||||
output.ImageSize = len(thumbnailData)
|
||||
output.ImageType = http.DetectContentType(thumbnailData)
|
||||
var err error
|
||||
output.ImageURL, output.ImageEncryption, err = getIntent(ctx).UploadMedia(
|
||||
|
|
|
|||
|
|
@ -66,10 +66,10 @@ func (mc *MessageConverter) convertTemplateMessage(ctx context.Context, info *ty
|
|||
if addButtonText {
|
||||
description += "\nUse the WhatsApp app to click buttons"
|
||||
}
|
||||
content = strings.TrimSpace(fmt.Sprintf("%s\n\n%s", content, description))
|
||||
content = fmt.Sprintf("%s\n\n%s", content, description)
|
||||
}
|
||||
if footer := tpl.GetHydratedFooterText(); footer != "" {
|
||||
content = strings.TrimSpace(fmt.Sprintf("%s\n\n%s", content, footer))
|
||||
content = fmt.Sprintf("%s\n\n%s", content, footer)
|
||||
}
|
||||
|
||||
var convertedTitle *bridgev2.ConvertedMessagePart
|
||||
|
|
@ -239,7 +239,7 @@ func (mc *MessageConverter) postProcessBusinessMessage(content string, headerMed
|
|||
converted.Content.Body += content
|
||||
contentHTML := parseWAFormattingToHTML(content, true)
|
||||
if contentHTML != event.TextToHTML(content) || converted.Content.FormattedBody != "" {
|
||||
converted.Content.Format = event.FormatHTML
|
||||
converted.Content.EnsureHasHTML()
|
||||
if converted.Content.FormattedBody != "" {
|
||||
converted.Content.FormattedBody += "<br><br>"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,8 @@
|
|||
package msgconv
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
|
|
@ -24,18 +26,21 @@ import (
|
|||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/exmime"
|
||||
"go.mau.fi/util/exslices"
|
||||
"go.mau.fi/util/lottie"
|
||||
"go.mau.fi/util/random"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/proto/waE2E"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/database"
|
||||
"maunium.net/go/mautrix/event"
|
||||
"maunium.net/go/mautrix/id"
|
||||
|
||||
"go.mau.fi/mautrix-whatsapp/pkg/waid"
|
||||
)
|
||||
|
|
@ -49,15 +54,11 @@ func (mc *MessageConverter) convertMediaMessage(
|
|||
cachedPart *bridgev2.ConvertedMessagePart,
|
||||
) (part *bridgev2.ConvertedMessagePart, contextInfo *waE2E.ContextInfo) {
|
||||
if mc.DisableViewOnce && isViewOnce {
|
||||
body := "You received a view once message. For added privacy, you can only open it on the WhatsApp app."
|
||||
if messageInfo.IsFromMe {
|
||||
body = "You sent a view once message from another device."
|
||||
}
|
||||
return &bridgev2.ConvertedMessagePart{
|
||||
Type: event.EventMessage,
|
||||
Content: &event.MessageEventContent{
|
||||
MsgType: event.MsgNotice,
|
||||
Body: body,
|
||||
Body: fmt.Sprintf("You received a view once %s. For added privacy, you can only open it on the WhatsApp app.", typeName),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
|
@ -79,18 +80,12 @@ func (mc *MessageConverter) convertMediaMessage(
|
|||
Type: whatsmeow.GetMediaType(msg),
|
||||
SHA256: msg.GetFileSHA256(),
|
||||
EncSHA256: msg.GetFileEncSHA256(),
|
||||
MimeType: msg.GetMimetype(),
|
||||
}
|
||||
if mc.DirectMedia {
|
||||
if preparedMedia.Info.MimeType == "application/was" {
|
||||
preparedMedia.Info.MimeType = "video/lottie+json"
|
||||
preparedMedia.FileName = "sticker.json"
|
||||
}
|
||||
preparedMedia.FillFileName()
|
||||
var err error
|
||||
portal := getPortal(ctx)
|
||||
idOverride := getEditTargetID(ctx)
|
||||
preparedMedia.URL, err = portal.Bridge.Matrix.GenerateContentURI(ctx, waid.MakeMediaID(messageInfo, idOverride, portal.Receiver))
|
||||
preparedMedia.URL, err = portal.Bridge.Matrix.GenerateContentURI(ctx, waid.MakeMediaID(messageInfo, portal.Receiver))
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to generate content URI: %w", err))
|
||||
}
|
||||
|
|
@ -119,28 +114,6 @@ func (mc *MessageConverter) convertMediaMessage(
|
|||
return
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) convertAlbumMessage(ctx context.Context, msg *waE2E.AlbumMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
|
||||
parts := make([]string, 0, 2)
|
||||
if msg.GetExpectedImageCount() > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%d images", msg.GetExpectedImageCount()))
|
||||
}
|
||||
if msg.GetExpectedVideoCount() > 0 {
|
||||
parts = append(parts, fmt.Sprintf("%d videos", msg.GetExpectedVideoCount()))
|
||||
}
|
||||
var partDesc string
|
||||
if len(parts) > 0 {
|
||||
partDesc = fmt.Sprintf(" with %s", strings.Join(parts, " and "))
|
||||
}
|
||||
body := fmt.Sprintf("Sent an album%s:", partDesc)
|
||||
return &bridgev2.ConvertedMessagePart{
|
||||
Type: event.EventMessage,
|
||||
Content: &event.MessageEventContent{
|
||||
MsgType: event.MsgNotice,
|
||||
Body: body,
|
||||
},
|
||||
}, msg.GetContextInfo()
|
||||
}
|
||||
|
||||
const FailedMediaField = "fi.mau.whatsapp.failed_media"
|
||||
|
||||
type FailedMediaKeys struct {
|
||||
|
|
@ -150,7 +123,6 @@ type FailedMediaKeys struct {
|
|||
SHA256 []byte `json:"sha256"`
|
||||
EncSHA256 []byte `json:"enc_sha256"`
|
||||
DirectPath string `json:"direct_path,omitempty"`
|
||||
MimeType string `json:"mime_type,omitempty"`
|
||||
}
|
||||
|
||||
func (f *FailedMediaKeys) GetDirectPath() string {
|
||||
|
|
@ -193,9 +165,7 @@ type PreparedMedia struct {
|
|||
}
|
||||
|
||||
func (pm *PreparedMedia) FillFileName() *PreparedMedia {
|
||||
if pm.Type == event.EventSticker {
|
||||
pm.FileName = ""
|
||||
} else if pm.FileName == "" {
|
||||
if pm.FileName == "" {
|
||||
pm.FileName = strings.TrimPrefix(string(pm.MsgType), "m.") + exmime.ExtensionFromMimetype(pm.Info.MimeType)
|
||||
}
|
||||
return pm
|
||||
|
|
@ -234,21 +204,6 @@ type MediaMessageWithDuration interface {
|
|||
GetSeconds() uint32
|
||||
}
|
||||
|
||||
const WhatsAppStickerSize = 190
|
||||
|
||||
func fixStickerDimensions(info *event.FileInfo) {
|
||||
if info.Width == info.Height {
|
||||
info.Width = WhatsAppStickerSize
|
||||
info.Height = WhatsAppStickerSize
|
||||
} else if info.Width > info.Height {
|
||||
info.Height /= info.Width / WhatsAppStickerSize
|
||||
info.Width = WhatsAppStickerSize
|
||||
} else {
|
||||
info.Width /= info.Height / WhatsAppStickerSize
|
||||
info.Height = WhatsAppStickerSize
|
||||
}
|
||||
}
|
||||
|
||||
func prepareMediaMessage(rawMsg MediaMessage) *PreparedMedia {
|
||||
extraInfo := map[string]any{}
|
||||
data := &PreparedMedia{
|
||||
|
|
@ -260,22 +215,6 @@ func prepareMediaMessage(rawMsg MediaMessage) *PreparedMedia {
|
|||
"info": extraInfo,
|
||||
},
|
||||
}
|
||||
if durationMsg, ok := rawMsg.(MediaMessageWithDuration); ok {
|
||||
data.Info.Duration = int(durationMsg.GetSeconds() * 1000)
|
||||
}
|
||||
if dimensionMsg, ok := rawMsg.(MediaMessageWithDimensions); ok {
|
||||
data.Info.Width = int(dimensionMsg.GetWidth())
|
||||
data.Info.Height = int(dimensionMsg.GetHeight())
|
||||
}
|
||||
if captionMsg, ok := rawMsg.(MediaMessageWithCaption); ok && captionMsg.GetCaption() != "" {
|
||||
data.Body = captionMsg.GetCaption()
|
||||
} else {
|
||||
data.Body = data.FileName
|
||||
}
|
||||
data.Info.Size = int(rawMsg.GetFileLength())
|
||||
data.Info.MimeType = rawMsg.GetMimetype()
|
||||
data.ContextInfo = rawMsg.GetContextInfo()
|
||||
|
||||
switch msg := rawMsg.(type) {
|
||||
case *waE2E.ImageMessage:
|
||||
data.MsgType = event.MsgImage
|
||||
|
|
@ -297,11 +236,12 @@ func prepareMediaMessage(rawMsg MediaMessage) *PreparedMedia {
|
|||
case *waE2E.StickerMessage:
|
||||
data.Type = event.EventSticker
|
||||
data.FileName = "sticker" + exmime.ExtensionFromMimetype(msg.GetMimetype())
|
||||
fixStickerDimensions(data.Info)
|
||||
if msg.GetMimetype() == "application/was" && data.FileName == "sticker" {
|
||||
data.FileName = "sticker.json"
|
||||
}
|
||||
case *waE2E.VideoMessage:
|
||||
data.MsgType = event.MsgVideo
|
||||
pairedMediaType := msg.GetContextInfo().GetPairedMediaType()
|
||||
if msg.GetGifPlayback() || pairedMediaType == waE2E.ContextInfo_MOTION_PHOTO_PARENT || pairedMediaType == waE2E.ContextInfo_MOTION_PHOTO_CHILD {
|
||||
if msg.GetGifPlayback() {
|
||||
extraInfo["fi.mau.gif"] = true
|
||||
extraInfo["fi.mau.loop"] = true
|
||||
extraInfo["fi.mau.autoplay"] = true
|
||||
|
|
@ -312,7 +252,22 @@ func prepareMediaMessage(rawMsg MediaMessage) *PreparedMedia {
|
|||
default:
|
||||
panic(fmt.Errorf("unknown media message type %T", rawMsg))
|
||||
}
|
||||
if durationMsg, ok := rawMsg.(MediaMessageWithDuration); ok {
|
||||
data.Info.Duration = int(durationMsg.GetSeconds() * 1000)
|
||||
}
|
||||
if dimensionMsg, ok := rawMsg.(MediaMessageWithDimensions); ok {
|
||||
data.Info.Width = int(dimensionMsg.GetWidth())
|
||||
data.Info.Height = int(dimensionMsg.GetHeight())
|
||||
}
|
||||
if captionMsg, ok := rawMsg.(MediaMessageWithCaption); ok && captionMsg.GetCaption() != "" {
|
||||
data.Body = captionMsg.GetCaption()
|
||||
} else {
|
||||
data.Body = data.FileName
|
||||
}
|
||||
|
||||
data.Info.Size = int(rawMsg.GetFileLength())
|
||||
data.Info.MimeType = rawMsg.GetMimetype()
|
||||
data.ContextInfo = rawMsg.GetContextInfo()
|
||||
return data
|
||||
}
|
||||
|
||||
|
|
@ -357,16 +312,13 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
|
|||
) error {
|
||||
client := getClient(ctx)
|
||||
intent := getIntent(ctx)
|
||||
var roomID id.RoomID
|
||||
if portal := getPortal(ctx); portal != nil {
|
||||
roomID = portal.MXID
|
||||
}
|
||||
portal := getPortal(ctx)
|
||||
var thumbnailData []byte
|
||||
var thumbnailInfo *event.FileInfo
|
||||
if part.Info.Size > uploadFileThreshold {
|
||||
var err error
|
||||
part.URL, part.File, err = intent.UploadMediaStream(ctx, roomID, -1, true, func(file io.Writer) (*bridgev2.FileStreamResult, error) {
|
||||
err := client.DownloadToFile(ctx, message, file.(*os.File))
|
||||
part.URL, part.File, err = intent.UploadMediaStream(ctx, portal.MXID, -1, true, func(file io.Writer) (*bridgev2.FileStreamResult, error) {
|
||||
err := client.DownloadToFile(message, file.(*os.File))
|
||||
if errors.Is(err, whatsmeow.ErrFileLengthMismatch) || errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Mismatching media checksums in message. Ignoring because WhatsApp seems to ignore them too")
|
||||
} else if err != nil {
|
||||
|
|
@ -387,7 +339,7 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
|
|||
return err
|
||||
}
|
||||
} else {
|
||||
data, err := client.Download(ctx, message)
|
||||
data, err := client.Download(message)
|
||||
if errors.Is(err, whatsmeow.ErrFileLengthMismatch) || errors.Is(err, whatsmeow.ErrInvalidMediaSHA256) {
|
||||
zerolog.Ctx(ctx).Warn().Err(err).Msg("Mismatching media checksums in message. Ignoring because WhatsApp seems to ignore them too")
|
||||
} else if err != nil {
|
||||
|
|
@ -398,14 +350,12 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
|
|||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else if part.Type == event.EventSticker && part.Info.MimeType == "image/webp" {
|
||||
mc.fillWebPStickerInfo(ctx, part, data)
|
||||
}
|
||||
if part.Info.MimeType == "" {
|
||||
part.Info.MimeType = http.DetectContentType(data)
|
||||
}
|
||||
part.FillFileName()
|
||||
part.URL, part.File, err = intent.UploadMedia(ctx, roomID, data, part.FileName, part.Info.MimeType)
|
||||
part.URL, part.File, err = intent.UploadMedia(ctx, portal.MXID, data, part.FileName, part.Info.MimeType)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%w: %w", bridgev2.ErrMediaReuploadFailed, err)
|
||||
}
|
||||
|
|
@ -414,7 +364,7 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
|
|||
var err error
|
||||
part.Info.ThumbnailURL, part.Info.ThumbnailFile, err = intent.UploadMedia(
|
||||
ctx,
|
||||
roomID,
|
||||
portal.MXID,
|
||||
thumbnailData,
|
||||
"thumbnail"+exmime.ExtensionFromMimetype(thumbnailInfo.MimeType),
|
||||
thumbnailInfo.MimeType,
|
||||
|
|
@ -428,6 +378,85 @@ func (mc *MessageConverter) reuploadWhatsAppAttachment(
|
|||
return nil
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) extractAnimatedSticker(fileInfo *PreparedMedia, data []byte) ([]byte, error) {
|
||||
zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read sticker zip: %w", err)
|
||||
}
|
||||
animationFile, err := zipReader.Open("animation/animation.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to open animation.json: %w", err)
|
||||
}
|
||||
animationFileInfo, err := animationFile.Stat()
|
||||
if err != nil {
|
||||
_ = animationFile.Close()
|
||||
return nil, fmt.Errorf("failed to stat animation.json: %w", err)
|
||||
} else if animationFileInfo.Size() > uploadFileThreshold {
|
||||
_ = animationFile.Close()
|
||||
return nil, fmt.Errorf("animation.json is too large (%.2f MiB)", float64(animationFileInfo.Size())/1024/1024)
|
||||
}
|
||||
data, err = io.ReadAll(animationFile)
|
||||
_ = animationFile.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read animation.json: %w", err)
|
||||
}
|
||||
fileInfo.Info.MimeType = "image/lottie+json"
|
||||
fileInfo.FileName = "sticker.json"
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) convertAnimatedSticker(ctx context.Context, fileInfo *PreparedMedia, data []byte) ([]byte, []byte, *event.FileInfo, error) {
|
||||
data, err := mc.extractAnimatedSticker(fileInfo, data)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
c := mc.AnimatedStickerConfig
|
||||
if c.Target == "disable" {
|
||||
return data, nil, nil, nil
|
||||
} else if !lottie.Supported() {
|
||||
zerolog.Ctx(ctx).Warn().Msg("Animated sticker conversion is enabled, but lottieconverter is not installed")
|
||||
return data, nil, nil, nil
|
||||
}
|
||||
input := bytes.NewReader(data)
|
||||
fileInfo.Info.MimeType = "image/" + c.Target
|
||||
fileInfo.FileName = "sticker." + c.Target
|
||||
switch c.Target {
|
||||
case "png":
|
||||
var output bytes.Buffer
|
||||
err = lottie.Convert(ctx, input, "", &output, c.Target, c.Args.Width, c.Args.Height, "1")
|
||||
return output.Bytes(), nil, nil, err
|
||||
case "gif":
|
||||
var output bytes.Buffer
|
||||
err = lottie.Convert(ctx, input, "", &output, c.Target, c.Args.Width, c.Args.Height, strconv.Itoa(c.Args.FPS))
|
||||
return output.Bytes(), nil, nil, err
|
||||
case "webm", "webp":
|
||||
tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("mautrix-whatsapp-lottieconverter-%s.%s", random.String(10), c.Target))
|
||||
defer func() {
|
||||
_ = os.Remove(tmpFile)
|
||||
}()
|
||||
thumbnailData, err := lottie.FFmpegConvert(ctx, input, tmpFile, c.Args.Width, c.Args.Height, c.Args.FPS)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
data, err = os.ReadFile(tmpFile)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("failed to read converted file: %w", err)
|
||||
}
|
||||
var thumbnailInfo *event.FileInfo
|
||||
if thumbnailData != nil {
|
||||
thumbnailInfo = &event.FileInfo{
|
||||
MimeType: "image/png",
|
||||
Width: c.Args.Width,
|
||||
Height: c.Args.Height,
|
||||
Size: len(thumbnailData),
|
||||
}
|
||||
}
|
||||
return data, thumbnailData, thumbnailInfo, nil
|
||||
default:
|
||||
return nil, nil, nil, fmt.Errorf("unsupported target format %s", c.Target)
|
||||
}
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) makeMediaFailure(ctx context.Context, mediaInfo *PreparedMedia, keys *FailedMediaKeys, err error) *bridgev2.ConvertedMessagePart {
|
||||
logLevel := zerolog.ErrorLevel
|
||||
var extra map[string]any
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ import (
|
|||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/exerrors"
|
||||
"go.mau.fi/util/ptr"
|
||||
"go.mau.fi/whatsmeow/proto/waAICommonDeprecated"
|
||||
"go.mau.fi/whatsmeow/proto/waE2E"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"google.golang.org/protobuf/proto"
|
||||
|
|
@ -92,8 +91,6 @@ func (mc *MessageConverter) convertGroupInviteMessage(ctx context.Context, info
|
|||
Code: msg.GetInviteCode(),
|
||||
Expiration: msg.GetInviteExpiration(),
|
||||
Inviter: info.Sender.ToNonAD(),
|
||||
GroupName: msg.GetGroupName(),
|
||||
IsParentGroup: msg.GetGroupType() == waE2E.GroupInviteMessage_PARENT,
|
||||
}
|
||||
extraAttrs = map[string]any{
|
||||
GroupInviteMetaField: inviteMeta,
|
||||
|
|
@ -117,11 +114,11 @@ func (mc *MessageConverter) convertGroupInviteMessage(ctx context.Context, info
|
|||
}, msg.GetContextInfo()
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) convertEphemeralSettingMessage(ctx context.Context, msg *waE2E.ProtocolMessage, ts time.Time, isBackfill bool) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
|
||||
func (mc *MessageConverter) convertEphemeralSettingMessage(ctx context.Context, msg *waE2E.ProtocolMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
|
||||
portal := getPortal(ctx)
|
||||
portalMeta := portal.Metadata.(*waid.PortalMetadata)
|
||||
disappear := database.DisappearingSetting{
|
||||
Type: event.DisappearingTypeAfterSend,
|
||||
Type: database.DisappearingTypeAfterRead,
|
||||
Timer: time.Duration(msg.GetEphemeralExpiration()) * time.Second,
|
||||
}
|
||||
if disappear.Timer == 0 {
|
||||
|
|
@ -129,39 +126,26 @@ func (mc *MessageConverter) convertEphemeralSettingMessage(ctx context.Context,
|
|||
}
|
||||
dontBridge := portal.Disappear == disappear
|
||||
content := bridgev2.DisappearingMessageNotice(disappear.Timer, false)
|
||||
if !isBackfill {
|
||||
if msg.EphemeralSettingTimestamp == nil || portalMeta.DisappearingTimerSetAt < msg.GetEphemeralSettingTimestamp() {
|
||||
portal.Disappear = disappear
|
||||
portalMeta.DisappearingTimerSetAt = msg.GetEphemeralSettingTimestamp()
|
||||
portal.UpdateDisappearingSetting(ctx, disappear, bridgev2.UpdateDisappearingSettingOpts{
|
||||
Sender: getIntent(ctx),
|
||||
Timestamp: ts,
|
||||
Implicit: false,
|
||||
Save: true,
|
||||
SendNotice: false,
|
||||
})
|
||||
err := portal.Save(ctx)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Err(err).Msg("Failed to save portal after updating expiration timer")
|
||||
}
|
||||
} else {
|
||||
content.Body += ", but the change was ignored."
|
||||
}
|
||||
}
|
||||
return &bridgev2.ConvertedMessagePart{
|
||||
Type: event.EventMessage,
|
||||
Content: content,
|
||||
Extra: map[string]any{
|
||||
"com.beeper.action_message": map[string]any{
|
||||
"type": "disappearing_timer",
|
||||
"timer": disappear.Timer.Milliseconds(),
|
||||
"timer_type": disappear.Type,
|
||||
"implicit": false,
|
||||
"backfill": isBackfill,
|
||||
},
|
||||
},
|
||||
DontBridge: dontBridge,
|
||||
}, nil
|
||||
}
|
||||
|
||||
const eventMessageTemplate = `
|
||||
{{- if .Name -}}
|
||||
<h4>{{ .Name }} {{- if .IsCanceled -}}<span> (Canceled)</span>{{- end -}}</h4>
|
||||
<h4>{{ .Name }}</h4>
|
||||
{{- end -}}
|
||||
{{- if .StartTime -}}
|
||||
<p>
|
||||
|
|
@ -187,7 +171,6 @@ var eventMessageTplParsed = exerrors.Must(template.New("eventmessage").Parse(str
|
|||
|
||||
type eventMessageParams struct {
|
||||
Name string
|
||||
IsCanceled bool
|
||||
JoinLink string
|
||||
StartTimeISO string
|
||||
StartTime string
|
||||
|
|
@ -200,7 +183,6 @@ type eventMessageParams struct {
|
|||
func (mc *MessageConverter) convertEventMessage(ctx context.Context, msg *waE2E.EventMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
|
||||
params := &eventMessageParams{
|
||||
Name: msg.GetName(),
|
||||
IsCanceled: msg.GetIsCanceled(),
|
||||
JoinLink: msg.GetJoinLink(),
|
||||
Location: msg.GetLocation().GetName(),
|
||||
DescriptionHTML: template.HTML(parseWAFormattingToHTML(msg.GetDescription(), false)),
|
||||
|
|
@ -232,53 +214,3 @@ func (mc *MessageConverter) convertEventMessage(ctx context.Context, msg *waE2E.
|
|||
Content: &content,
|
||||
}, msg.GetContextInfo()
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) convertPinInChatMessage(ctx context.Context, msg *waE2E.PinInChatMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
|
||||
body := "Pinned a message"
|
||||
if msg.GetType() == waE2E.PinInChatMessage_UNPIN_FOR_ALL {
|
||||
body = "Unpinned a message"
|
||||
}
|
||||
|
||||
return &bridgev2.ConvertedMessagePart{
|
||||
Type: event.EventMessage,
|
||||
Content: &event.MessageEventContent{
|
||||
MsgType: event.MsgNotice,
|
||||
Body: body,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) convertKeepInChatMessage(ctx context.Context, msg *waE2E.KeepInChatMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
|
||||
body := "Kept a message"
|
||||
if msg.GetKeepType() == waE2E.KeepType_UNDO_KEEP_FOR_ALL {
|
||||
body = "Unkept a message"
|
||||
}
|
||||
|
||||
return &bridgev2.ConvertedMessagePart{
|
||||
Type: event.EventMessage,
|
||||
Content: &event.MessageEventContent{
|
||||
MsgType: event.MsgNotice,
|
||||
Body: body,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) convertRichResponseMessage(ctx context.Context, msg *waE2E.AIRichResponseMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
|
||||
var body strings.Builder
|
||||
|
||||
// TODO switch to new format?
|
||||
for i, submsg := range msg.GetSubmessages() {
|
||||
if submsg.GetMessageType() == waAICommonDeprecated.AIRichResponseSubMessageType_AI_RICH_RESPONSE_TEXT {
|
||||
if i > 0 {
|
||||
body.WriteString("\n")
|
||||
}
|
||||
body.WriteString(submsg.GetMessageText())
|
||||
}
|
||||
}
|
||||
|
||||
content := format.RenderMarkdown(body.String(), true, false)
|
||||
return &bridgev2.ConvertedMessagePart{
|
||||
Type: event.EventMessage,
|
||||
Content: &content,
|
||||
}, msg.GetContextInfo()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"go.mau.fi/util/ptr"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/proto/waCommon"
|
||||
"go.mau.fi/whatsmeow/proto/waE2E"
|
||||
|
|
@ -94,31 +95,7 @@ func (mc *MessageConverter) convertPollCreationMessage(ctx context.Context, msg
|
|||
}, msg.GetContextInfo()
|
||||
}
|
||||
|
||||
func rerouteMessageKey(ctx context.Context, chat, sender types.JID, groupLIDAddressing bool) types.JID {
|
||||
if store := getClient(ctx).Store; store != nil && chat.Server == types.DefaultUserServer && sender.Server == types.HiddenUserServer {
|
||||
senderPN, _ := store.LIDs.GetPNForLID(ctx, sender)
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Stringer("orig_participant", sender).
|
||||
Stringer("rerouted_participant", senderPN).
|
||||
Msg("Rerouting message key (PN recipient in LID DM)")
|
||||
if !senderPN.IsEmpty() {
|
||||
return senderPN
|
||||
}
|
||||
} else if store != nil && chat.Server == types.GroupServer && sender.Server == types.DefaultUserServer && groupLIDAddressing {
|
||||
senderLID, _ := store.LIDs.GetLIDForPN(ctx, sender)
|
||||
zerolog.Ctx(ctx).Debug().
|
||||
Stringer("orig_participant", sender).
|
||||
Stringer("rerouted_participant", senderLID).
|
||||
Msg("Rerouting message key (PN recipient in LID group)")
|
||||
if !senderLID.IsEmpty() {
|
||||
return senderLID
|
||||
}
|
||||
}
|
||||
return sender
|
||||
}
|
||||
|
||||
func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender types.JID, key *waCommon.MessageKey) networkid.MessageID {
|
||||
groupLIDAddressing := sender.Server == types.HiddenUserServer
|
||||
func KeyToMessageID(client *whatsmeow.Client, chat, sender types.JID, key *waCommon.MessageKey) networkid.MessageID {
|
||||
sender = sender.ToNonAD()
|
||||
var err error
|
||||
if !key.GetFromMe() {
|
||||
|
|
@ -132,21 +109,14 @@ func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender
|
|||
sender.Server = types.DefaultUserServer
|
||||
}
|
||||
} else if chat.Server == types.DefaultUserServer || chat.Server == types.BotServer {
|
||||
if sender.User == client.Store.GetJID().User || sender.User == client.Store.GetLID().User {
|
||||
// Message key is not from the sender, but message sender (containing key) is me,
|
||||
// so message key sender is the other user in the DM
|
||||
ownID := ptr.Val(client.Store.ID).ToNonAD()
|
||||
if sender.User == ownID.User {
|
||||
sender = chat
|
||||
} else {
|
||||
// Message key is not from the sender, but message sender (containing key) is not me,
|
||||
// so message key sender is me
|
||||
sender = client.Store.GetJID().ToNonAD()
|
||||
sender = ownID
|
||||
}
|
||||
} else {
|
||||
zerolog.Ctx(ctx).Warn().
|
||||
Stringer("chat", chat).
|
||||
Stringer("sender", sender).
|
||||
Any("key", key).
|
||||
Msg("Failed to get message ID from key")
|
||||
// TODO log somehow?
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
|
@ -157,10 +127,6 @@ func KeyToMessageID(ctx context.Context, client *whatsmeow.Client, chat, sender
|
|||
chat = remoteJID
|
||||
}
|
||||
}
|
||||
sender = rerouteMessageKey(
|
||||
context.WithValue(ctx, contextKeyClient, client),
|
||||
chat, sender, groupLIDAddressing,
|
||||
)
|
||||
return waid.MakeMessageID(chat, sender, key.GetID())
|
||||
}
|
||||
|
||||
|
|
@ -172,16 +138,13 @@ var failedPollUpdatePart = &bridgev2.ConvertedMessagePart{
|
|||
|
||||
func (mc *MessageConverter) convertPollUpdateMessage(ctx context.Context, info *types.MessageInfo, msg *waE2E.PollUpdateMessage) (*bridgev2.ConvertedMessagePart, *waE2E.ContextInfo) {
|
||||
log := zerolog.Ctx(ctx)
|
||||
pollMessageID := KeyToMessageID(ctx, getClient(ctx), info.Chat, info.Sender, msg.PollCreationMessageKey)
|
||||
pollMessageID := KeyToMessageID(getClient(ctx), info.Chat, info.Sender, msg.PollCreationMessageKey)
|
||||
pollMessage, err := mc.Bridge.DB.Message.GetPartByID(ctx, getPortal(ctx).Receiver, pollMessageID, "")
|
||||
if err != nil {
|
||||
log.Err(err).Msg("Failed to get poll update target message")
|
||||
return failedPollUpdatePart, nil
|
||||
} else if pollMessage == nil {
|
||||
log.Warn().Str("target_message_id", string(pollMessageID)).Msg("Poll update target message not found")
|
||||
return failedPollUpdatePart, nil
|
||||
}
|
||||
vote, err := getClient(ctx).DecryptPollVote(ctx, &events.Message{
|
||||
vote, err := getClient(ctx).DecryptPollVote(&events.Message{
|
||||
Info: *info,
|
||||
Message: &waE2E.Message{PollUpdateMessage: msg},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,455 +0,0 @@
|
|||
// mautrix-whatsapp - A Matrix-WhatsApp puppeting bridge.
|
||||
// Copyright (C) 2026 Tulir Asokan
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Affero General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Affero General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Affero General Public License
|
||||
// along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
package msgconv
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"encoding/binary"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/rs/zerolog"
|
||||
"github.com/tidwall/gjson"
|
||||
"go.mau.fi/util/exstrings"
|
||||
"go.mau.fi/util/lottie"
|
||||
"go.mau.fi/util/random"
|
||||
"go.mau.fi/whatsmeow"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
"maunium.net/go/mautrix"
|
||||
"maunium.net/go/mautrix/bridgev2"
|
||||
"maunium.net/go/mautrix/bridgev2/database"
|
||||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
"maunium.net/go/mautrix/event"
|
||||
|
||||
"go.mau.fi/mautrix-whatsapp/pkg/waid"
|
||||
)
|
||||
|
||||
func (mc *MessageConverter) GetCachedStickerPack(ctx context.Context, client *whatsmeow.Client, packID string) (*types.StickerPack, error) {
|
||||
mc.stickerPackCacheLock.Lock()
|
||||
defer mc.stickerPackCacheLock.Unlock()
|
||||
cached, ok := mc.stickerPackCache[packID]
|
||||
if ok {
|
||||
if cached == nil {
|
||||
return nil, bridgev2.RespError(mautrix.MNotFound.WithMessage("sticker pack not found (cached)"))
|
||||
}
|
||||
return cached, nil
|
||||
}
|
||||
|
||||
pack, err := client.FetchStickerPack(ctx, packID)
|
||||
if errors.Is(err, whatsmeow.ErrMediaDownloadFailedWith404) {
|
||||
mc.stickerPackCache[packID] = nil
|
||||
return nil, bridgev2.WrapRespErr(err, mautrix.MNotFound)
|
||||
} else if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
mc.stickerPackCache[packID] = pack
|
||||
if packID != pack.StickerPackID {
|
||||
mc.stickerPackCache[pack.StickerPackID] = pack
|
||||
}
|
||||
return pack, nil
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) GetCachedSticker(ctx context.Context, client *whatsmeow.Client, packID string, hash []byte) (*types.StickerPackItem, error) {
|
||||
pack, err := mc.GetCachedStickerPack(ctx, client, packID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
for _, sticker := range pack.Stickers {
|
||||
if bytes.Equal(sticker.FileHash, hash) {
|
||||
return sticker, nil
|
||||
}
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) DownloadImagePack(ctx context.Context, userLoginID networkid.UserLoginID, client *whatsmeow.Client, inputURL string) (*bridgev2.ImportedImagePack, error) {
|
||||
parsedURL, err := url.Parse(inputURL)
|
||||
if err != nil {
|
||||
return nil, bridgev2.WrapRespErr(err, mautrix.MNotFound)
|
||||
} else if parsedURL.Host != "api.whatsapp.com" && parsedURL.Host != "wa.me" {
|
||||
return nil, bridgev2.WrapRespErr(fmt.Errorf("invalid host %q", parsedURL.Host), mautrix.MNotFound)
|
||||
} else if !strings.HasPrefix(parsedURL.Path, "/stickerpack/") {
|
||||
return nil, bridgev2.WrapRespErr(fmt.Errorf("invalid path %q", parsedURL.Path), mautrix.MNotFound)
|
||||
}
|
||||
packName := strings.Split(strings.TrimPrefix(parsedURL.Path, "/stickerpack/"), "/")[0]
|
||||
if packName == "" {
|
||||
return nil, bridgev2.WrapRespErr(fmt.Errorf("empty pack name"), mautrix.MNotFound)
|
||||
}
|
||||
pack, err := mc.GetCachedStickerPack(ctx, client, packName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
canonicalURL := "https://wa.me/stickerpack/" + pack.StickerPackID
|
||||
topLevelExtra := map[string]any{
|
||||
"fi.mau.whatsapp.stickerpack": map[string]any{
|
||||
"id": pack.StickerPackID,
|
||||
"name": pack.Name,
|
||||
"description": pack.Description,
|
||||
"publisher": pack.Publisher,
|
||||
"animated": pack.Animated > 0,
|
||||
"lottie": pack.Lottie > 0,
|
||||
},
|
||||
}
|
||||
content := &event.ImagePackEventContent{
|
||||
Images: make(map[string]*event.ImagePackImage, len(pack.Stickers)),
|
||||
Metadata: event.ImagePackMetadata{
|
||||
DisplayName: pack.Name,
|
||||
AvatarURL: "",
|
||||
Usage: []event.ImagePackUsage{event.ImagePackUsageSticker},
|
||||
Attribution: fmt.Sprintf("By %s on WhatsApp %s", pack.Publisher, canonicalURL),
|
||||
BridgedPack: &event.BridgedStickerPack{
|
||||
Network: StickerSourceID,
|
||||
URL: canonicalURL,
|
||||
},
|
||||
},
|
||||
}
|
||||
ctx = context.WithValue(ctx, contextKeyClient, client)
|
||||
ctx = context.WithValue(ctx, contextKeyIntent, mc.Bridge.Bot)
|
||||
ctx = context.WithValue(ctx, contextKeyPortal, (*bridgev2.Portal)(nil))
|
||||
for i, sticker := range pack.Stickers {
|
||||
shortcode := sticker.PreviewWebpID
|
||||
if shortcode == "" {
|
||||
shortcode = fmt.Sprintf("%s_img%d", pack.StickerPackID, i+1)
|
||||
}
|
||||
body := sticker.AccessibilityText
|
||||
var emoji string
|
||||
if len(sticker.Emojis) > 0 {
|
||||
emoji = sticker.Emojis[0]
|
||||
if body == "" {
|
||||
body = strings.Join(sticker.Emojis, " ")
|
||||
}
|
||||
}
|
||||
part := &PreparedMedia{
|
||||
Type: event.EventSticker,
|
||||
MessageEventContent: &event.MessageEventContent{
|
||||
Body: body,
|
||||
Info: &event.FileInfo{
|
||||
MimeType: sticker.MimeType,
|
||||
Width: sticker.Width,
|
||||
Height: sticker.Height,
|
||||
Size: int(sticker.FileSize),
|
||||
BridgedSticker: &event.BridgedSticker{
|
||||
Network: StickerSourceID,
|
||||
ID: base64.StdEncoding.EncodeToString(sticker.FileHash),
|
||||
Emoji: emoji,
|
||||
PackURL: canonicalURL,
|
||||
},
|
||||
},
|
||||
},
|
||||
TypeDescription: "sticker",
|
||||
}
|
||||
dbKey := database.Key(fmt.Sprintf("stickercache:%x", part.Info.BridgedSticker.ID))
|
||||
fixStickerDimensions(part.Info)
|
||||
var packed *event.ImagePackImage
|
||||
if mc.DirectMedia {
|
||||
dbKey = ""
|
||||
if part.Info.MimeType == "application/was" {
|
||||
part.Info.MimeType = "video/lottie+json"
|
||||
}
|
||||
part.URL, err = mc.Bridge.Matrix.GenerateContentURI(ctx, waid.MakeStickerPackMediaID(pack.StickerPackID, sticker.FileHash, userLoginID))
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("failed to generate content URI: %w", err))
|
||||
}
|
||||
} else if cached := mc.Bridge.DB.KV.Get(ctx, dbKey); cached != "" {
|
||||
err = json.Unmarshal([]byte(cached), &packed)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal cached sticker data: %w", err)
|
||||
}
|
||||
} else {
|
||||
err = mc.reuploadWhatsAppAttachment(ctx, sticker, part)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to reupload sticker %q: %w", sticker.GetDirectPath(), err)
|
||||
}
|
||||
}
|
||||
if packed == nil {
|
||||
packed = &event.ImagePackImage{
|
||||
URL: part.URL,
|
||||
Body: part.Body,
|
||||
Info: part.Info,
|
||||
}
|
||||
if dbKey != "" {
|
||||
data, _ := json.Marshal(packed)
|
||||
if data != nil {
|
||||
mc.Bridge.DB.KV.Set(ctx, dbKey, string(data))
|
||||
}
|
||||
}
|
||||
}
|
||||
content.Images[shortcode] = packed
|
||||
}
|
||||
|
||||
return &bridgev2.ImportedImagePack{
|
||||
Content: content,
|
||||
Extra: topLevelExtra,
|
||||
Shortcode: pack.StickerPackID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type StickerMetadata struct {
|
||||
StickerPackID string `json:"sticker-pack-id"`
|
||||
AccessibilityText string `json:"accessibility-text"`
|
||||
Emojis []string `json:"emojis"`
|
||||
IsFirstPartySticker int `json:"is-first-party-sticker"`
|
||||
}
|
||||
|
||||
func (sm *StickerMetadata) ToMatrix(content *event.MessageEventContent) {
|
||||
if sm == nil {
|
||||
return
|
||||
}
|
||||
if sm.StickerPackID != "" && content.Info.BridgedSticker == nil {
|
||||
content.Info.BridgedSticker = &event.BridgedSticker{
|
||||
Network: StickerSourceID,
|
||||
PackURL: StickerPackURLPrefix + sm.StickerPackID,
|
||||
}
|
||||
if len(sm.Emojis) > 0 {
|
||||
content.Info.BridgedSticker.Emoji = sm.Emojis[0]
|
||||
}
|
||||
}
|
||||
if sm.AccessibilityText != "" {
|
||||
content.Body = sm.AccessibilityText
|
||||
} else if len(sm.Emojis) > 0 {
|
||||
content.Body = strings.Join(sm.Emojis, " ")
|
||||
}
|
||||
}
|
||||
|
||||
const StickerSourceID = "whatsapp"
|
||||
const StickerPackURLPrefix = "https://wa.me/stickerpack/"
|
||||
|
||||
func PackAnimatedSticker(data []byte) ([]byte, error) {
|
||||
var buf bytes.Buffer
|
||||
zipWriter := zip.NewWriter(&buf)
|
||||
f, err := zipWriter.Create("animation/animation.json")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create zip entry: %w", err)
|
||||
}
|
||||
_, err = f.Write(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to write zip entry: %w", err)
|
||||
}
|
||||
err = zipWriter.Close()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to close zip writer: %w", err)
|
||||
}
|
||||
return buf.Bytes(), nil
|
||||
}
|
||||
|
||||
func ExtractAnimatedSticker(data []byte) ([]byte, *StickerMetadata, error) {
|
||||
zipReader, err := zip.NewReader(bytes.NewReader(data), int64(len(data)))
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read sticker zip: %w", err)
|
||||
}
|
||||
animationFile, err := zipReader.Open("animation/animation.json")
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to open animation.json: %w", err)
|
||||
}
|
||||
animationFileInfo, err := animationFile.Stat()
|
||||
if err != nil {
|
||||
_ = animationFile.Close()
|
||||
return nil, nil, fmt.Errorf("failed to stat animation.json: %w", err)
|
||||
} else if animationFileInfo.Size() > uploadFileThreshold {
|
||||
_ = animationFile.Close()
|
||||
return nil, nil, fmt.Errorf("animation.json is too large (%.2f MiB)", float64(animationFileInfo.Size())/1024/1024)
|
||||
}
|
||||
data, err = io.ReadAll(animationFile)
|
||||
_ = animationFile.Close()
|
||||
if err != nil {
|
||||
return nil, nil, fmt.Errorf("failed to read animation.json: %w", err)
|
||||
}
|
||||
var meta StickerMetadata
|
||||
metaFile, err := zipReader.Open("animation/animation.json.overridden_metadata")
|
||||
if err == nil {
|
||||
_ = json.NewDecoder(metaFile).Decode(&meta)
|
||||
_ = metaFile.Close()
|
||||
}
|
||||
if meta.StickerPackID == "" {
|
||||
res := gjson.GetBytes(data, "metadata.customProps")
|
||||
if res.IsObject() {
|
||||
_ = json.Unmarshal(exstrings.UnsafeBytes(res.Raw), &meta)
|
||||
}
|
||||
}
|
||||
return data, &meta, nil
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) extractAnimatedSticker(fileInfo *PreparedMedia, data []byte) ([]byte, error) {
|
||||
data, meta, err := ExtractAnimatedSticker(data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
meta.ToMatrix(fileInfo.MessageEventContent)
|
||||
fileInfo.Info.MimeType = "video/lottie+json"
|
||||
fileInfo.FileName = "sticker.json"
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) convertAnimatedSticker(ctx context.Context, fileInfo *PreparedMedia, data []byte) ([]byte, []byte, *event.FileInfo, error) {
|
||||
data, err := mc.extractAnimatedSticker(fileInfo, data)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
c := mc.AnimatedStickerConfig
|
||||
if c.Target == "disable" {
|
||||
return data, nil, nil, nil
|
||||
} else if !lottie.Supported() {
|
||||
zerolog.Ctx(ctx).Warn().Msg("Animated sticker conversion is enabled, but lottieconverter is not installed")
|
||||
return data, nil, nil, nil
|
||||
}
|
||||
input := bytes.NewReader(data)
|
||||
fileInfo.Info.MimeType = "image/" + c.Target
|
||||
fileInfo.FileName = "sticker." + c.Target
|
||||
switch c.Target {
|
||||
case "png":
|
||||
var output bytes.Buffer
|
||||
err = lottie.Convert(ctx, input, "", &output, c.Target, c.Args.Width, c.Args.Height, "1")
|
||||
return output.Bytes(), nil, nil, err
|
||||
case "gif":
|
||||
var output bytes.Buffer
|
||||
err = lottie.Convert(ctx, input, "", &output, c.Target, c.Args.Width, c.Args.Height, strconv.Itoa(c.Args.FPS))
|
||||
return output.Bytes(), nil, nil, err
|
||||
case "webm", "webp":
|
||||
tmpFile := filepath.Join(os.TempDir(), fmt.Sprintf("mautrix-whatsapp-lottieconverter-%s.%s", random.String(10), c.Target))
|
||||
defer func() {
|
||||
_ = os.Remove(tmpFile)
|
||||
}()
|
||||
thumbnailData, err := lottie.FFmpegConvert(ctx, input, tmpFile, c.Args.Width, c.Args.Height, c.Args.FPS)
|
||||
if err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
data, err = os.ReadFile(tmpFile)
|
||||
if err != nil {
|
||||
return nil, nil, nil, fmt.Errorf("failed to read converted file: %w", err)
|
||||
}
|
||||
var thumbnailInfo *event.FileInfo
|
||||
if thumbnailData != nil {
|
||||
thumbnailInfo = &event.FileInfo{
|
||||
MimeType: "image/png",
|
||||
Width: c.Args.Width,
|
||||
Height: c.Args.Height,
|
||||
Size: len(thumbnailData),
|
||||
}
|
||||
}
|
||||
return data, thumbnailData, thumbnailInfo, nil
|
||||
default:
|
||||
return nil, nil, nil, fmt.Errorf("unsupported target format %s", c.Target)
|
||||
}
|
||||
}
|
||||
|
||||
func (mc *MessageConverter) fillWebPStickerInfo(ctx context.Context, fileInfo *PreparedMedia, data []byte) {
|
||||
meta, err := extractWebPStickerMetadata(data)
|
||||
if err != nil {
|
||||
zerolog.Ctx(ctx).Debug().Err(err).Msg("Failed to extract webp sticker metadata")
|
||||
return
|
||||
}
|
||||
meta.ToMatrix(fileInfo.MessageEventContent)
|
||||
}
|
||||
|
||||
// stickerMetadataEXIFTag is the custom EXIF tag WhatsApp uses to embed
|
||||
// sticker pack metadata as a JSON object inside non-animated webp stickers.
|
||||
const stickerMetadataEXIFTag = 0x5741
|
||||
|
||||
// extractWebPStickerMetadata parses the WhatsApp sticker pack metadata JSON
|
||||
// embedded in EXIF tag 0x5741 of a non-animated webp sticker.
|
||||
func extractWebPStickerMetadata(data []byte) (*StickerMetadata, error) {
|
||||
exif, err := findWebPChunk(data, "EXIF")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
raw, err := findEXIFTagValue(exif, stickerMetadataEXIFTag)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var meta StickerMetadata
|
||||
err = json.Unmarshal(raw, &meta)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse sticker metadata JSON: %w", err)
|
||||
}
|
||||
return &meta, nil
|
||||
}
|
||||
|
||||
func findWebPChunk(data []byte, chunkType string) ([]byte, error) {
|
||||
if len(data) < 12 || string(data[0:4]) != "RIFF" || string(data[8:12]) != "WEBP" {
|
||||
return nil, fmt.Errorf("not a webp file")
|
||||
}
|
||||
for pos := 12; pos+8 <= len(data); {
|
||||
size := binary.LittleEndian.Uint32(data[pos+4 : pos+8])
|
||||
start := pos + 8
|
||||
end := start + int(size)
|
||||
if end > len(data) {
|
||||
return nil, fmt.Errorf("webp chunk %q extends past end of file", data[pos:pos+4])
|
||||
}
|
||||
if string(data[pos:pos+4]) == chunkType {
|
||||
return data[start:end], nil
|
||||
}
|
||||
pos = end
|
||||
if pos%2 != 0 {
|
||||
pos++
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("webp chunk %q not found", chunkType)
|
||||
}
|
||||
|
||||
func findEXIFTagValue(exif []byte, tag uint16) ([]byte, error) {
|
||||
if len(exif) < 8 {
|
||||
return nil, fmt.Errorf("exif data too short")
|
||||
}
|
||||
var bo binary.ByteOrder
|
||||
switch string(exif[0:2]) {
|
||||
case "II":
|
||||
bo = binary.LittleEndian
|
||||
case "MM":
|
||||
bo = binary.BigEndian
|
||||
default:
|
||||
return nil, fmt.Errorf("invalid TIFF byte order %q", exif[0:2])
|
||||
}
|
||||
if bo.Uint16(exif[2:4]) != 0x002A {
|
||||
return nil, fmt.Errorf("invalid TIFF magic")
|
||||
}
|
||||
ifdOffset := int(bo.Uint32(exif[4:8]))
|
||||
if ifdOffset < 0 || ifdOffset+2 > len(exif) {
|
||||
return nil, fmt.Errorf("IFD offset out of range")
|
||||
}
|
||||
count := int(bo.Uint16(exif[ifdOffset : ifdOffset+2]))
|
||||
entries := ifdOffset + 2
|
||||
if entries+count*12 > len(exif) {
|
||||
return nil, fmt.Errorf("IFD entries out of range")
|
||||
}
|
||||
for i := 0; i < count; i++ {
|
||||
entry := exif[entries+i*12 : entries+(i+1)*12]
|
||||
if bo.Uint16(entry[0:2]) != tag {
|
||||
continue
|
||||
}
|
||||
// Tag 0x5741 stores JSON as type 7 (UNDEFINED), where size == count bytes.
|
||||
size := int(bo.Uint32(entry[4:8]))
|
||||
if size <= 4 {
|
||||
return entry[8 : 8+size], nil
|
||||
}
|
||||
offset := int(bo.Uint32(entry[8:12]))
|
||||
if offset+size > len(exif) {
|
||||
return nil, fmt.Errorf("exif tag value out of range")
|
||||
}
|
||||
return exif[offset : offset+size], nil
|
||||
}
|
||||
return nil, fmt.Errorf("exif tag 0x%04x not found", tag)
|
||||
}
|
||||
|
|
@ -20,30 +20,24 @@ import (
|
|||
"crypto/ecdh"
|
||||
"crypto/rand"
|
||||
"encoding/json"
|
||||
"time"
|
||||
|
||||
"go.mau.fi/util/exerrors"
|
||||
"go.mau.fi/util/jsontime"
|
||||
"go.mau.fi/util/random"
|
||||
"go.mau.fi/whatsmeow/appstate"
|
||||
"go.mau.fi/whatsmeow/types"
|
||||
)
|
||||
|
||||
type UserLoginMetadata struct {
|
||||
WADeviceID uint16 `json:"wa_device_id"`
|
||||
WALID string `json:"wa_lid"`
|
||||
PhoneLastSeen jsontime.Unix `json:"phone_last_seen"`
|
||||
PhoneLastPinged jsontime.Unix `json:"phone_last_pinged"`
|
||||
Timezone string `json:"timezone"`
|
||||
PushKeys *PushKeys `json:"push_keys,omitempty"`
|
||||
APNSEncPubKey []byte `json:"apns_enc_pubkey,omitempty"`
|
||||
APNSEncPrivKey []byte `json:"apns_enc_privkey,omitempty"`
|
||||
LoggedInAt jsontime.Unix `json:"logged_in_at,omitempty"`
|
||||
|
||||
AppStateRecoveryAttempted map[appstate.WAPatchName]time.Time `json:"app_state_recovery_attempted,omitempty"`
|
||||
|
||||
HistorySyncPortalsNeedCreating bool `json:"history_sync_portals_need_creating,omitempty"`
|
||||
|
||||
MData json.RawMessage `json:"mdata,omitempty"`
|
||||
}
|
||||
|
||||
type PushKeys struct {
|
||||
|
|
@ -74,9 +68,6 @@ type GroupInviteMeta struct {
|
|||
Code string `json:"code"`
|
||||
Expiration int64 `json:"expiration,string"`
|
||||
Inviter types.JID `json:"inviter"`
|
||||
|
||||
GroupName string `json:"group_name,omitempty"`
|
||||
IsParentGroup bool `json:"is_parent_group,omitempty"`
|
||||
}
|
||||
|
||||
type MessageMetadata struct {
|
||||
|
|
@ -115,11 +106,9 @@ type ReactionMetadata struct {
|
|||
|
||||
type PortalMetadata struct {
|
||||
DisappearingTimerSetAt int64 `json:"disappearing_timer_set_at,omitempty"`
|
||||
TopicID string `json:"topic_id,omitempty"`
|
||||
LastSync jsontime.Unix `json:"last_sync,omitempty"`
|
||||
CommunityAnnouncementGroup bool `json:"is_cag,omitempty"`
|
||||
AddressingMode types.AddressingMode `json:"addressing_mode,omitempty"`
|
||||
LIDMigrationAttempted bool `json:"lid_migration_attempted,omitempty"`
|
||||
}
|
||||
|
||||
type GhostMetadata struct {
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@
|
|||
package waid
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
|
|
@ -28,26 +29,12 @@ import (
|
|||
"maunium.net/go/mautrix/bridgev2/networkid"
|
||||
)
|
||||
|
||||
const (
|
||||
// Media ID types start from 255, because old media IDs didn't have a type byte and had the length at the start.
|
||||
mediaIDTypeMessage = 255
|
||||
mediaIDTypeAvatar = 254
|
||||
mediaIDTypeCommunityAvatar = 253
|
||||
mediaIDTypeStickerPackItem = 252
|
||||
)
|
||||
|
||||
func MakeMediaID(messageInfo *types.MessageInfo, idOverride types.MessageID, receiver networkid.UserLoginID) networkid.MediaID {
|
||||
func MakeMediaID(messageInfo *types.MessageInfo, receiver networkid.UserLoginID) networkid.MediaID {
|
||||
compactChat := compactJID(messageInfo.Chat.ToNonAD())
|
||||
compactSender := compactJID(messageInfo.Sender.ToNonAD())
|
||||
receiverID := compactJID(ParseUserLoginID(receiver, 0))
|
||||
var compactID []byte
|
||||
if idOverride != "" {
|
||||
compactID = compactMsgID(idOverride)
|
||||
} else {
|
||||
compactID = compactMsgID(messageInfo.ID)
|
||||
}
|
||||
mediaID := make([]byte, 0, 5+len(compactChat)+len(compactSender)+len(receiverID)+len(compactID))
|
||||
mediaID = append(mediaID, mediaIDTypeMessage)
|
||||
compactID := compactMsgID(messageInfo.ID)
|
||||
mediaID := make([]byte, 0, 3+len(compactChat)+len(compactSender)+len(compactID))
|
||||
mediaID = append(mediaID, byte(len(compactChat)))
|
||||
mediaID = append(mediaID, compactChat...)
|
||||
mediaID = append(mediaID, byte(len(compactSender)))
|
||||
|
|
@ -59,131 +46,29 @@ func MakeMediaID(messageInfo *types.MessageInfo, idOverride types.MessageID, rec
|
|||
return mediaID
|
||||
}
|
||||
|
||||
func MakeAvatarMediaID(targetJID types.JID, id string, receiver networkid.UserLoginID, community bool) networkid.MediaID {
|
||||
compactTarget := compactJID(targetJID.ToNonAD())
|
||||
receiverID := compactJID(ParseUserLoginID(receiver, 0))
|
||||
mediaID := make([]byte, 0, 4+len(compactTarget)+len(id)+len(receiverID))
|
||||
if community {
|
||||
mediaID = append(mediaID, mediaIDTypeCommunityAvatar)
|
||||
} else {
|
||||
mediaID = append(mediaID, mediaIDTypeAvatar)
|
||||
}
|
||||
mediaID = append(mediaID, byte(len(compactTarget)))
|
||||
mediaID = append(mediaID, compactTarget...)
|
||||
mediaID = append(mediaID, byte(len(id)))
|
||||
mediaID = append(mediaID, id...)
|
||||
mediaID = append(mediaID, byte(len(receiverID)))
|
||||
mediaID = append(mediaID, receiverID...)
|
||||
return mediaID
|
||||
}
|
||||
|
||||
type AvatarMediaInfo struct {
|
||||
TargetJID types.JID
|
||||
AvatarID string
|
||||
Community bool
|
||||
}
|
||||
|
||||
func MakeStickerPackMediaID(packID string, fileHash []byte, receiver networkid.UserLoginID) networkid.MediaID {
|
||||
receiverID := compactJID(ParseUserLoginID(receiver, 0))
|
||||
mediaID := make([]byte, 0, 4+len(packID)+len(fileHash)+len(receiverID))
|
||||
mediaID = append(mediaID, mediaIDTypeStickerPackItem)
|
||||
mediaID = append(mediaID, byte(len(packID)))
|
||||
mediaID = append(mediaID, packID...)
|
||||
mediaID = append(mediaID, byte(len(fileHash)))
|
||||
mediaID = append(mediaID, fileHash...)
|
||||
mediaID = append(mediaID, byte(len(receiverID)))
|
||||
mediaID = append(mediaID, receiverID...)
|
||||
return mediaID
|
||||
}
|
||||
|
||||
type StickerPackMediaInfo struct {
|
||||
PackID string
|
||||
FileHash []byte
|
||||
}
|
||||
|
||||
type ParsedMediaID struct {
|
||||
Message *ParsedMessageID
|
||||
Avatar *AvatarMediaInfo
|
||||
Sticker *StickerPackMediaInfo
|
||||
UserLogin networkid.UserLoginID
|
||||
}
|
||||
|
||||
func ParseMediaID(mediaID networkid.MediaID) (*ParsedMediaID, error) {
|
||||
mediaIDType := mediaIDTypeMessage
|
||||
if mediaID[0] > 127 {
|
||||
mediaIDType = int(mediaID[0])
|
||||
mediaID = mediaID[1:]
|
||||
}
|
||||
var parsed ParsedMediaID
|
||||
switch mediaIDType {
|
||||
case mediaIDTypeMessage:
|
||||
chatJID, err := readCompact(&mediaID, parseCompactJID)
|
||||
func ParseMediaID(mediaID networkid.MediaID) (*ParsedMessageID, networkid.UserLoginID, error) {
|
||||
reader := bytes.NewReader(mediaID)
|
||||
chatJID, err := readCompact(reader, parseCompactJID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse chat JID: %w", err)
|
||||
return nil, "", fmt.Errorf("failed to parse chat JID: %w", err)
|
||||
}
|
||||
senderJID, err := readCompact(&mediaID, parseCompactJID)
|
||||
senderJID, err := readCompact(reader, parseCompactJID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse sender JID: %w", err)
|
||||
return nil, "", fmt.Errorf("failed to parse sender JID: %w", err)
|
||||
}
|
||||
receiverID, err := readCompact(&mediaID, parseCompactJID)
|
||||
receiverID, err := readCompact(reader, parseCompactJID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse receiver JID: %w", err)
|
||||
return nil, "", fmt.Errorf("failed to parse receiver JID: %w", err)
|
||||
}
|
||||
id, err := readCompact(&mediaID, parseCompactMsgID)
|
||||
id, err := readCompact(reader, parseCompactMsgID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse message ID: %w", err)
|
||||
return nil, "", fmt.Errorf("failed to parse message ID: %w", err)
|
||||
}
|
||||
parsed.Message = &ParsedMessageID{
|
||||
return &ParsedMessageID{
|
||||
Chat: chatJID,
|
||||
Sender: senderJID,
|
||||
ID: id,
|
||||
}
|
||||
parsed.UserLogin = MakeUserLoginID(receiverID)
|
||||
case mediaIDTypeAvatar, mediaIDTypeCommunityAvatar:
|
||||
targetJID, err := readCompact(&mediaID, parseCompactJID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse target JID: %w", err)
|
||||
}
|
||||
avatarID, err := readCompact(&mediaID, parseString)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse avatar ID: %w", err)
|
||||
}
|
||||
receiverID, err := readCompact(&mediaID, parseCompactJID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse receiver JID: %w", err)
|
||||
}
|
||||
parsed.Avatar = &AvatarMediaInfo{
|
||||
TargetJID: targetJID,
|
||||
AvatarID: avatarID,
|
||||
Community: mediaIDType == mediaIDTypeCommunityAvatar,
|
||||
}
|
||||
parsed.UserLogin = MakeUserLoginID(receiverID)
|
||||
case mediaIDTypeStickerPackItem:
|
||||
packID, err := readCompact(&mediaID, parseString)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse sticker pack ID: %w", err)
|
||||
}
|
||||
fileHash, err := readCompact(&mediaID, rawBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse sticker file hash: %w", err)
|
||||
}
|
||||
receiverID, err := readCompact(&mediaID, parseCompactJID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse receiver JID: %w", err)
|
||||
}
|
||||
parsed.Sticker = &StickerPackMediaInfo{
|
||||
PackID: packID,
|
||||
FileHash: fileHash,
|
||||
}
|
||||
parsed.UserLogin = MakeUserLoginID(receiverID)
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown media ID type %d", mediaIDType)
|
||||
}
|
||||
return &parsed, nil
|
||||
}
|
||||
|
||||
func parseString(data []byte) (string, error) {
|
||||
return string(data), nil
|
||||
}, MakeUserLoginID(receiverID), nil
|
||||
}
|
||||
|
||||
func isUpperHex(str string) bool {
|
||||
|
|
@ -284,20 +169,16 @@ func parseCompactJID(jid []byte) (types.JID, error) {
|
|||
}
|
||||
}
|
||||
|
||||
func rawBytes(data []byte) ([]byte, error) {
|
||||
return data, nil
|
||||
}
|
||||
|
||||
func readCompact[T any](data *networkid.MediaID, fn func(data []byte) (T, error)) (T, error) {
|
||||
func readCompact[T any](reader *bytes.Reader, fn func(data []byte) (T, error)) (T, error) {
|
||||
var defVal T
|
||||
if len(*data) < 1 {
|
||||
return defVal, fmt.Errorf("%w (data too short to read length)", io.ErrUnexpectedEOF)
|
||||
length, err := reader.ReadByte()
|
||||
if err != nil {
|
||||
return defVal, err
|
||||
}
|
||||
length := int((*data)[0])
|
||||
if len(*data) < length+1 {
|
||||
return defVal, fmt.Errorf("%w (wanted %d+1 bytes, only have %d)", io.ErrUnexpectedEOF, length, len(*data))
|
||||
data := make([]byte, length)
|
||||
_, err = io.ReadFull(reader, data)
|
||||
if err != nil {
|
||||
return defVal, err
|
||||
}
|
||||
dataToParse := (*data)[1 : length+1]
|
||||
*data = (*data)[length+1:]
|
||||
return fn(dataToParse)
|
||||
return fn(data)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue