Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
189 commits
Select commit Hold shift + click to select a range
92024bd
build: pin generated business MEX bindings
purpshell Aug 9, 2026
a29379a
feat: add Business App catalog reads
purpshell Aug 9, 2026
c254d1c
fix: harden Business App order reads
purpshell Aug 9, 2026
1d5619d
test: validate Business App reads through Barback
purpshell Aug 9, 2026
92aa3be
test: wait for authenticated Barback session
purpshell Aug 9, 2026
ada407a
build: make repository pre-commit clean
purpshell Aug 9, 2026
26dd418
fix: preserve collection paging metadata
purpshell Aug 9, 2026
c765b54
fix: build benchmark against baseline libraries
purpshell Aug 10, 2026
5fef1e9
fix: detect legacy benchmark candidates
purpshell Aug 10, 2026
b8fcd80
fix: align benchmark API detection
purpshell Aug 10, 2026
19ea671
fix: refresh upstream benchmark socket patch
purpshell Aug 10, 2026
ef4d45b
fix(bench): skip business smoke on legacy baselines
purpshell Aug 10, 2026
3f3a2bd
fix(bench): report skipped business smoke
purpshell Aug 10, 2026
8054915
Merge pull request #6 from polymorfa/codex/smb-app-parity
purpshell Aug 10, 2026
a2af2fa
feat: add strict newsletter deletion
purpshell Aug 9, 2026
93c03d7
fix: bound newsletter deletion JIDs
purpshell Aug 9, 2026
3ece35a
build: format MEX binding test
purpshell Aug 9, 2026
b37ee4f
fix: reject unsupported desktop channel deletion
purpshell Aug 9, 2026
735bf43
Merge pull request #7 from polymorfa/codex/channel-delete-mex
purpshell Aug 10, 2026
f5bd3fb
feat: batch chat label mutations
purpshell Aug 9, 2026
17b99ca
feat: selectively emit label full-sync events
purpshell Aug 9, 2026
5511bb5
fix: deduplicate atomic label changes
purpshell Aug 9, 2026
6c3ca48
Merge pull request #8 from polymorfa/codex/atomic-label-replacement
purpshell Aug 10, 2026
b91c2ac
feat: control history sync acknowledgements
purpshell Aug 9, 2026
1376c08
feat: defer history sync side effects
purpshell Aug 9, 2026
29741e9
fix: retain history deletion nonce
purpshell Aug 9, 2026
3d134e7
fix: detach asynchronous nonce persistence
purpshell Aug 9, 2026
e53e091
fix: keep async history sync nonblocking
purpshell Aug 10, 2026
398f33f
fix: serialize async history nonce saves
purpshell Aug 10, 2026
2532eda
Merge pull request #9 from polymorfa/codex/history-sync-ack-control
purpshell Aug 10, 2026
96d7518
Add typed business profile mutations
purpshell Aug 9, 2026
f8dcb55
Redact business profile data from stanza logs
purpshell Aug 9, 2026
5cf55e1
fix: preserve business profile semantics
purpshell Aug 9, 2026
677fb8a
fix: harden business profile updates
purpshell Aug 9, 2026
4a7375f
fix: wrap cover photo profile mutations
purpshell Aug 9, 2026
c2df0ed
fix: normalize IQ error node semantics
purpshell Aug 10, 2026
2c423ab
fix(errors): normalize empty IQ child lists
purpshell Aug 10, 2026
9aad620
fix(errors): normalize encoded attribute scalars
purpshell Aug 10, 2026
0d51568
fix: normalize and redact node scalar content
purpshell Aug 10, 2026
725771f
Merge pull request #10 from polymorfa/codex/business-profile-mutation
purpshell Aug 10, 2026
63346ba
feat: add business catalog product mutations
purpshell Aug 9, 2026
85ee37f
fix: deliver business nonces out of band
purpshell Aug 9, 2026
7ce6700
fix: suppress queued nonce redelivery
purpshell Aug 9, 2026
f04491d
fix: redact product upload transport errors
purpshell Aug 9, 2026
4923339
fix: harden product upload validation
purpshell Aug 9, 2026
bdec5aa
fix: make business token waits cancelable
purpshell Aug 10, 2026
c9a38c5
fix: make business token invalidation cancelable
purpshell Aug 10, 2026
13cf06c
fix: send business mutation actor IDs
purpshell Aug 10, 2026
f3285ec
fix(business): classify HTTP auth failures before JSON
purpshell Aug 10, 2026
50944dd
fix: redact business access tokens
purpshell Aug 10, 2026
48facd9
fix: redact business session cookies
purpshell Aug 10, 2026
b76eab8
fix: redact business token nonces
purpshell Aug 10, 2026
a507131
test: cover business credential redaction
purpshell Aug 10, 2026
d5ea089
Merge pull request #11 from polymorfa/codex/business-product-mutations
purpshell Aug 10, 2026
7f85e4b
feat(business): add collection mutations
purpshell Aug 9, 2026
6c36a63
Merge pull request #12 from polymorfa/codex/business-collection-mutat…
purpshell Aug 10, 2026
3fc15fa
Add business commerce controls
purpshell Aug 9, 2026
ba1f743
fix: decode created business catalogs
purpshell Aug 9, 2026
5d80b31
Merge pull request #13 from polymorfa/codex/business-commerce-controls
purpshell Aug 10, 2026
ba09c3f
Add merchant compliance operations
purpshell Aug 9, 2026
b4de298
fix: require merchant entity names
purpshell Aug 10, 2026
04b36f0
test: seed business mutation actor
purpshell Aug 10, 2026
27ba46f
fix: require merchant entity classification
purpshell Aug 10, 2026
16134b9
Merge pull request #14 from polymorfa/codex/business-merchant-compliance
purpshell Aug 10, 2026
b4be587
Add quick reply app state support
purpshell Aug 9, 2026
579c875
build: format label event test
purpshell Aug 9, 2026
ceea524
fix: preserve quick reply label associations
purpshell Aug 9, 2026
78effba
Merge pull request #15 from polymorfa/codex/business-quick-replies
purpshell Aug 10, 2026
1639950
feat: add typed business message builders
purpshell Aug 9, 2026
35a01b8
fix: preserve business message context
purpshell Aug 9, 2026
532b56f
build: format business message imports
purpshell Aug 9, 2026
cf3c315
fix: validate business message builders
purpshell Aug 9, 2026
8280d4d
fix: require business order tokens
purpshell Aug 9, 2026
b0cbfcf
fix: enforce business message limits
purpshell Aug 9, 2026
74ec8eb
fix: align business builder contracts
purpshell Aug 9, 2026
60e74f9
fix: preserve explicit zero product prices
purpshell Aug 9, 2026
04bf3a6
fix: enforce single-select list limits
purpshell Aug 10, 2026
6a826c9
fix: require product section titles
purpshell Aug 10, 2026
db7514e
fix: preserve optional business fields
purpshell Aug 10, 2026
55608cf
fix: preserve sale price presence
purpshell Aug 10, 2026
03f5db1
fix: reject oversized list sections early
purpshell Aug 10, 2026
8b2b4b9
Merge pull request #16 from polymorfa/codex/business-message-builders
purpshell Aug 10, 2026
c9bc4ac
Add business account capability reads
purpshell Aug 9, 2026
f20cd71
fix: redact linked account payloads
purpshell Aug 10, 2026
e1b884f
Merge pull request #17 from polymorfa/codex/business-account-capabili…
purpshell Aug 10, 2026
485493b
Fix business metadata on response messages
purpshell Aug 9, 2026
1651224
Merge pull request #18 from polymorfa/codex/business-response-biz-node
purpshell Aug 10, 2026
3cfb5ff
Skip empty PN to LID migration transactions
purpshell Aug 9, 2026
b858771
Keep SQLStore regression dependency-free
purpshell Aug 9, 2026
62f83af
fix: keep PN migration preflight race-safe
purpshell Aug 9, 2026
dee78d0
fix: bound empty PN migration queries
purpshell Aug 9, 2026
fdc31f4
perf(sqlstore): bind PN migration prefix patterns
purpshell Aug 10, 2026
f9ddf7b
perf(sqlstore): use SQLite prefix ranges
purpshell Aug 10, 2026
2f25c8b
perf(sqlstore): index postgres prefix lookups
purpshell Aug 10, 2026
32439d8
Merge pull request #19 from polymorfa/codex/skip-empty-pn-lid-transac…
purpshell Aug 10, 2026
a56740b
Harden malformed binary node decoding
purpshell Aug 9, 2026
399be55
fix: reject empty odd packed strings
purpshell Aug 9, 2026
243b828
Merge pull request #20 from polymorfa/codex/malformed-node-hardening
purpshell Aug 10, 2026
1908453
Serialize device save and delete
purpshell Aug 9, 2026
c784273
fix: make device persistence waits cancelable
purpshell Aug 10, 2026
058bf9e
fix: recheck device lock cancellation
purpshell Aug 10, 2026
73fadd5
Merge pull request #21 from polymorfa/codex/device-save-delete-lock
purpshell Aug 10, 2026
6399c0a
Expose participant hash mismatches
purpshell Aug 9, 2026
f3adc4c
Merge pull request #22 from polymorfa/codex/send-phash-mismatch
purpshell Aug 10, 2026
c6f8e7f
Add phone number consent message builders
purpshell Aug 9, 2026
6370ba3
Merge pull request #23 from polymorfa/codex/phone-number-consent
purpshell Aug 10, 2026
3e53320
Use LIDs as Signal identities and support usernames
purpshell Aug 9, 2026
083bcc3
fix: preserve device when resolving LIDs
purpshell Aug 9, 2026
ff0ceba
fix: persist LID contact mutations
purpshell Aug 9, 2026
d2dfea8
fix: advance username schema migration
purpshell Aug 10, 2026
77b6c48
fix: complete LID contact integration
purpshell Aug 10, 2026
d386234
fix: distinguish authoritative usernames
purpshell Aug 10, 2026
fc28dc1
fix: preserve username migration compatibility
purpshell Aug 10, 2026
7ec0fbc
Merge pull request #24 from polymorfa/codex/lid-signal-identities
purpshell Aug 10, 2026
83dd433
Batch username alias persistence
purpshell Aug 9, 2026
ace6126
Expose username aliases in identity lookups
purpshell Aug 9, 2026
ea2d72c
fix: persist username aliases compatibly
purpshell Aug 9, 2026
af0478c
fix: serialize username batch writes
purpshell Aug 9, 2026
b42f258
fix: keep username persistence best-effort
purpshell Aug 10, 2026
8085653
fix: persist participant response usernames
purpshell Aug 10, 2026
6ff9467
fix: continue username fallback writes
purpshell Aug 10, 2026
869dfa3
fix(groups): persist aliases from direct responses
purpshell Aug 10, 2026
291af6f
fix(groups): persist aliases from membership changes
purpshell Aug 10, 2026
ee92fe7
fix(groups): persist linked-community aliases
purpshell Aug 10, 2026
11124b7
fix(groups): persist pending-request aliases
purpshell Aug 10, 2026
f0f6775
fix(store): keep latest batched username
purpshell Aug 10, 2026
4df6bb2
Merge pull request #25 from polymorfa/codex/username-alias-persistence
purpshell Aug 10, 2026
f772cc7
Batch reverse LID alias lookups
purpshell Aug 9, 2026
2d41474
fix(store): preserve reverse lookup compatibility
purpshell Aug 10, 2026
8029e1f
fix(sqlstore): chunk reverse LID lookups
purpshell Aug 10, 2026
1417213
Merge pull request #26 from polymorfa/codex/batch-lid-alias-lookup
purpshell Aug 10, 2026
f7e221d
Validate phone number consent with Barback
purpshell Aug 9, 2026
5725560
build: format Barback benchmark imports
purpshell Aug 9, 2026
664e324
test: validate both phone consent messages
purpshell Aug 9, 2026
2d29839
test: fail phone consent smoke runs
purpshell Aug 9, 2026
a5b452f
fix: wake benchmark on fatal validation
purpshell Aug 9, 2026
8861e6f
fix: share phone consent validation failures
purpshell Aug 9, 2026
3603a3f
fix: keep phone consent baseline compatible
purpshell Aug 10, 2026
3011aad
fix: isolate phone consent benchmark smoke
purpshell Aug 10, 2026
af1eeef
fix(bench): cancel phone consent with run
purpshell Aug 10, 2026
28458c1
docs(bench): describe both consent messages
purpshell Aug 10, 2026
273c239
fix(bench): isolate consent accounting
purpshell Aug 10, 2026
3aa85c6
Preserve preflight database metrics
purpshell Aug 10, 2026
3ae4ca8
Isolate consent smoke database metrics
purpshell Aug 10, 2026
33de930
Detect consent benchmark compatibility
purpshell Aug 10, 2026
0eef4c9
Merge pull request #27 from polymorfa/codex/lid-primary-phone-request
purpshell Aug 10, 2026
a86e015
Add LID identity verification codes
purpshell Aug 9, 2026
3ca82ab
Validate security codes with Barback
purpshell Aug 9, 2026
b84c358
Expose stable LID resolution
purpshell Aug 9, 2026
3164703
Avoid redundant self identity fetch
purpshell Aug 9, 2026
1175c60
Preserve local device zero without registration
purpshell Aug 9, 2026
4845ecb
fix: preserve concurrent identity cache writes
purpshell Aug 9, 2026
9e65efc
fix: harden identity verification state
purpshell Aug 9, 2026
478d83f
test: serialize security code smoke failures
purpshell Aug 9, 2026
4f1e146
fix: tolerate oversized identity reader results
purpshell Aug 9, 2026
4c7ff56
fix: update LID-only device caches
purpshell Aug 9, 2026
344f6a8
fix: invalidate mapped Signal identities
purpshell Aug 10, 2026
57230a6
fix: isolate candidate benchmark smoke
purpshell Aug 10, 2026
406ad06
fix(bench): skip security smoke on legacy baselines
purpshell Aug 10, 2026
3d3ba94
fix(bench): scope security smoke to candidate
purpshell Aug 10, 2026
25aa7b1
fix(bench): gate security smoke on build tags
purpshell Aug 10, 2026
9610e78
fix: isolate identity verification state
purpshell Aug 10, 2026
baf1ba8
fix: reject identities fetched before deletion
purpshell Aug 10, 2026
599491f
Merge pull request #29 from polymorfa/codex/security-code-api
purpshell Aug 10, 2026
8e23f7d
feat: add typed native flow messages
purpshell Aug 9, 2026
779a840
build: format send test imports
purpshell Aug 9, 2026
fd8f400
fix: preserve native flow payload semantics
purpshell Aug 9, 2026
e6ba734
fix: unwrap extended view-once flows
purpshell Aug 9, 2026
a222df9
fix: unwrap list business metadata
purpshell Aug 9, 2026
dce0461
fix: enforce address flow text limits
purpshell Aug 10, 2026
3389e22
fix: enforce flow text limits
purpshell Aug 10, 2026
3da127f
fix: reject invalid flow button text
purpshell Aug 10, 2026
c8ac2bc
fix: preserve flow payload text
purpshell Aug 10, 2026
e6744f0
test: reject trailing flow JSON
purpshell Aug 10, 2026
0c67c75
fix: preserve mixed native flow metadata
purpshell Aug 10, 2026
3a5780d
fix: preserve explicit empty flow data
purpshell Aug 10, 2026
73b9a0d
Merge pull request #30 from polymorfa/codex/native-flow-messages
purpshell Aug 10, 2026
b5e5ba3
build: publish HyperMeow as its own Go module
purpshell Aug 10, 2026
7d1e837
docs: explain the hypermeow module / whatsmeow package split
purpshell Aug 10, 2026
3ea2d78
docs: warn that only one whatsmeow may be linked per binary
purpshell Aug 10, 2026
6eaf529
docs: use 'go mod why -m' for the coexistence check
purpshell Aug 10, 2026
92b49be
docs: negate the coexistence grep so unsafe graphs fail
purpshell Aug 10, 2026
b86af76
docs: do not let a failed go list read as a clean check
purpshell Aug 10, 2026
45788c7
docs: scan test deps and keep the coexistence check POSIX
purpshell Aug 10, 2026
2a5d862
docs: drop the pipeline from the coexistence check
purpshell Aug 10, 2026
aeb9eb2
build: tidy go.sum
purpshell Aug 10, 2026
9da6911
Merge pull request #33 from polymorfa/codex/module-path-hypermeow
purpshell Aug 10, 2026
4c1c118
release: prepare v0.1.0
purpshell Aug 10, 2026
e2f25d6
fix(bench): support legacy module baselines
purpshell Aug 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
*.pb.go linguist-generated=true
*.pb.raw binary linguist-generated=true
internals.go linguist-generated=true
*.patch -whitespace
3 changes: 3 additions & 0 deletions .github/workflows/go.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ jobs:
with:
go-version: ${{ matrix.go-version }}

- name: Check generated MEX bindings
run: go run ./internal/cmd/genmex -input mex/spec.json -output mex/bindings.go -check

- name: Build
run: go build -v ./...

Expand Down
5 changes: 3 additions & 2 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@ repos:
hooks:
- id: trailing-whitespace
exclude_types: [markdown]
exclude: LICENSE
exclude: '(^LICENSE$|\.patch$)'
- id: end-of-file-fixer
exclude: LICENSE
- id: check-yaml
exclude: ^benchmark/barback/compose\.(dm|local-cache)\.yaml$
- id: check-added-large-files

- repo: https://github.com/tekwizely/pre-commit-golang
Expand All @@ -16,7 +17,7 @@ repos:
- id: go-imports-repo
args:
- "-local"
- "go.mau.fi/whatsmeow"
- "github.com/polymorfa/hypermeow"
- "-w"
- id: go-vet-repo-mod
# TODO enable this
Expand Down
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,56 @@

HyperMeow is a library used at Polymorfa to ship WhatsApp at scale. We forked from tulir's project since these performance changes are somewhat experimental and diverge from tulir's minimalist philosophy. For Polymorfa to succeed, we needed all the WhatsApp Web functions in one place, meanwhile tulir prefers the core functionalities / messaging be the scope of whatsmeow.

HyperMeow is its own Go module, imported directly as `github.com/polymorfa/hypermeow`. It no longer requires a `replace` directive:

```go
import whatsmeow "github.com/polymorfa/hypermeow"
```

```sh
go get github.com/polymorfa/hypermeow
```

### The module is `hypermeow`, the package is `whatsmeow`

Only the *module path* moved. The Go *package* names are unchanged from upstream, so the root package is still declared `package whatsmeow`. Both of these are expected:

- godoc renders the title as **"whatsmeow package - github.com/polymorfa/hypermeow"**;
- the import path's last element (`hypermeow`) does not match the package name (`whatsmeow`), so import the root package under an explicit alias as shown above.

This is deliberate. Keeping the upstream package names means no call site changes when migrating - `whatsmeow.Client`, `whatsmeow.NewClient` and friends all still resolve - and merges from tulir's upstream do not conflict on the package clause of every file. Sub-packages are unaffected, since their path element already matches their package name (`.../store` is `package store`, and so on).

### Only one whatsmeow may be linked into a binary

HyperMeow keeps upstream's generated protobuf descriptor paths (`waCommon/WACommon.proto` and friends) and their symbol namespaces. Those are registered in a process-global registry, so linking HyperMeow **and** upstream `go.mau.fi/whatsmeow` into the same binary panics before `main`:

```
proto: file "waCommon/WACommon.proto" is already registered
```

The old `replace` arrangement made this impossible, because both import paths resolved to one module. A distinct module path removes that guarantee, so a partially migrated dependency graph — where one of your dependencies still requires `go.mau.fi/whatsmeow` — is not safe.

The failure is loud and immediate rather than silent, but it surfaces at process start. Check for it at build time instead:

```sh
go mod why -m go.mau.fi/whatsmeow # should report the module is not needed
```

Use `-m`. Without it, `go mod why` asks about the *package* `go.mau.fi/whatsmeow`, and a dependency that imports only a subpackage — say `go.mau.fi/whatsmeow/proto/waCommon` — makes it answer "main module does not need package" while the upstream module is linked and its descriptors still collide.

A stricter check inspects the link graph directly:

```sh
deps="$(go list -deps -test ./...)" || exit 1
case "$deps" in *go.mau.fi/whatsmeow*) exit 1 ;; esac
```

Exit 0 means safe. There is deliberately no pipeline and no external command here. `-test` matters because `go list -deps` omits test-only dependencies, and a project importing upstream only from a `_test.go` still links both copies under `go test`. Capturing the output first means a failed `go list` propagates through `||` instead of being mistaken for an empty result, and matching with `case` avoids `grep`, whose exit status is 0 on a match — so a naive form of this check passes precisely when the graph is unsafe.

or assert it in a test via `debug.ReadBuildInfo()`, failing if any entry in `Deps` reports the module path `go.mau.fi/whatsmeow`.

Migrating from the previous `replace go.mau.fi/whatsmeow => github.com/polymorfa/hypermeow` setup: drop the `replace` line, add a normal `require` on `github.com/polymorfa/hypermeow`, and rewrite `go.mau.fi/whatsmeow` import paths to `github.com/polymorfa/hypermeow`. A `replace` directive is only honoured in the main module, so the previous arrangement did not carry to anything that depended on your module in turn; a direct requirement does.

The reproducible Barback and PostgreSQL benchmark is documented in [`benchmark/barback`](benchmark/barback/README.md).

## Discussion
Expand Down
86 changes: 71 additions & 15 deletions appstate.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,13 @@ import (
"go.mau.fi/util/exslices"
"go.mau.fi/util/ptr"

"go.mau.fi/whatsmeow/appstate"
waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/proto/waE2E"
"go.mau.fi/whatsmeow/proto/waServerSync"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/types/events"
"github.com/polymorfa/hypermeow/appstate"
waBinary "github.com/polymorfa/hypermeow/binary"
"github.com/polymorfa/hypermeow/proto/waE2E"
"github.com/polymorfa/hypermeow/proto/waServerSync"
"github.com/polymorfa/hypermeow/store"
"github.com/polymorfa/hypermeow/types"
"github.com/polymorfa/hypermeow/types/events"
)

// FetchAppState fetches updates to the given type of app state. If fullSync is true, the current
Expand Down Expand Up @@ -67,7 +67,7 @@ func (cli *Client) fetchAppState(ctx context.Context, name appstate.WAPatchName,
wantSnapshot := fullSync
var eventsToDispatch []any
eventsToDispatchPtr := &eventsToDispatch
if fullSync && !cli.EmitAppStateEventsOnFullSync {
if fullSync && !cli.EmitAppStateEventsOnFullSync && !cli.EmitLabelEventsOnFullSync && !cli.EmitQuickReplyEventsOnFullSync {
eventsToDispatchPtr = nil
}
for hasMore {
Expand Down Expand Up @@ -108,7 +108,7 @@ func (cli *Client) handleAppStateRecovery(
}
var eventsToDispatch []any
eventsToDispatchPtr := &eventsToDispatch
if !cli.EmitAppStateEventsOnFullSync {
if !cli.EmitAppStateEventsOnFullSync && !cli.EmitLabelEventsOnFullSync && !cli.EmitQuickReplyEventsOnFullSync {
eventsToDispatchPtr = nil
}
snapshot, err := appstate.ParseRecovery(result[0].GetSyncdSnapshotFatalRecoveryResponse())
Expand Down Expand Up @@ -186,28 +186,62 @@ func (cli *Client) collectEventsToDispatch(
}
}
for _, mutation := range mutations {
if eventsToDispatch != nil && mutation.Operation == waServerSync.SyncdMutation_SET {
emitMutation := eventsToDispatch != nil && (!fullSync || cli.shouldEmitFullSyncMutation(mutation.Index))
if emitMutation && mutation.Operation == waServerSync.SyncdMutation_SET {
*eventsToDispatch = append(*eventsToDispatch, &events.AppState{Index: mutation.Index, SyncActionValue: mutation.Action})
}
evt := cli.dispatchAppState(ctx, name, mutation, fullSync)
if eventsToDispatch != nil && evt != nil {
if emitMutation && evt != nil {
*eventsToDispatch = append(*eventsToDispatch, evt)
}
}
return nil
}

func (cli *Client) shouldEmitFullSyncMutation(index []string) bool {
if cli.EmitAppStateEventsOnFullSync {
return true
}
if len(index) == 0 {
return false
}
if cli.EmitQuickReplyEventsOnFullSync && index[0] == appstate.IndexQuickReply {
return true
}
if !cli.EmitLabelEventsOnFullSync {
return false
}
switch index[0] {
case appstate.IndexLabelEdit, appstate.IndexLabelAssociationChat, appstate.IndexLabelAssociationMessage:
return true
default:
return false
}
}

func (cli *Client) filterContacts(mutations []appstate.Mutation) ([]appstate.Mutation, []store.ContactEntry) {
filteredMutations := mutations[:0]
contacts := make([]store.ContactEntry, 0, len(mutations))
for _, mutation := range mutations {
if mutation.Index[0] == "contact" && len(mutation.Index) > 1 {
if len(mutation.Index) > 1 && mutation.Index[0] == appstate.IndexContact {
jid, _ := types.ParseJID(mutation.Index[1])
act := mutation.Action.GetContactAction()
contacts = append(contacts, store.ContactEntry{
JID: jid,
FirstName: act.GetFirstName(),
FullName: act.GetFullName(),
JID: jid,
FirstName: act.GetFirstName(),
FullName: act.GetFullName(),
Username: act.GetUsername(),
UsernameSet: true,
})
} else if len(mutation.Index) > 1 && mutation.Index[0] == appstate.IndexLIDContact {
jid, _ := types.ParseJID(mutation.Index[1])
act := mutation.Action.GetLidContactAction()
contacts = append(contacts, store.ContactEntry{
JID: jid,
FirstName: act.GetFirstName(),
FullName: act.GetFullName(),
Username: act.GetUsername(),
UsernameSet: true,
})
} else {
filteredMutations = append(filteredMutations, mutation)
Expand Down Expand Up @@ -291,6 +325,18 @@ func (cli *Client) dispatchAppState(ctx context.Context, name appstate.WAPatchNa
eventToDispatch = &events.Contact{JID: jid, Timestamp: ts, Action: act, FromFullSync: fullSync}
if cli.Store.Contacts != nil {
storeUpdateError = cli.Store.Contacts.PutContactName(ctx, jid, act.GetFirstName(), act.GetFullName())
if usernameStore, ok := cli.Store.Contacts.(store.ContactUsernameStore); ok && storeUpdateError == nil {
storeUpdateError = usernameStore.PutContactUsername(ctx, jid, act.GetUsername())
}
}
case appstate.IndexLIDContact:
act := mutation.Action.GetLidContactAction()
eventToDispatch = &events.LIDContact{JID: jid, Timestamp: ts, Action: act, FromFullSync: fullSync}
if cli.Store.Contacts != nil {
storeUpdateError = cli.Store.Contacts.PutContactName(ctx, jid, act.GetFirstName(), act.GetFullName())
if usernameStore, ok := cli.Store.Contacts.(store.ContactUsernameStore); ok && storeUpdateError == nil {
storeUpdateError = usernameStore.PutContactUsername(ctx, jid, act.GetUsername())
}
}
case appstate.IndexClearChat:
act := mutation.Action.GetClearChatAction()
Expand Down Expand Up @@ -417,6 +463,16 @@ func (cli *Client) dispatchAppState(ctx context.Context, name appstate.WAPatchNa
Action: act,
FromFullSync: fullSync,
}
case appstate.IndexQuickReply:
if len(mutation.Index) < 2 {
return
}
eventToDispatch = &events.QuickReply{
Timestamp: ts,
ID: mutation.Index[1],
Action: mutation.Action.GetQuickReplyAction(),
FromFullSync: fullSync,
}
}
if storeUpdateError != nil {
cli.Log.Errorf("Failed to update device store after app state mutation: %v", storeUpdateError)
Expand Down
10 changes: 5 additions & 5 deletions appstate/decode.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,11 @@ import (

"google.golang.org/protobuf/proto"

waBinary "go.mau.fi/whatsmeow/binary"
"go.mau.fi/whatsmeow/proto/waServerSync"
"go.mau.fi/whatsmeow/proto/waSyncAction"
"go.mau.fi/whatsmeow/store"
"go.mau.fi/whatsmeow/util/cbcutil"
waBinary "github.com/polymorfa/hypermeow/binary"
"github.com/polymorfa/hypermeow/proto/waServerSync"
"github.com/polymorfa/hypermeow/proto/waSyncAction"
"github.com/polymorfa/hypermeow/store"
"github.com/polymorfa/hypermeow/util/cbcutil"
)

// PatchList represents a decoded response to getting app state patches from the WhatsApp servers.
Expand Down
67 changes: 62 additions & 5 deletions appstate/encode.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@ import (

"google.golang.org/protobuf/proto"

"go.mau.fi/whatsmeow/proto/waCommon"
"go.mau.fi/whatsmeow/proto/waServerSync"
"go.mau.fi/whatsmeow/proto/waSyncAction"
"go.mau.fi/whatsmeow/types"
"go.mau.fi/whatsmeow/util/cbcutil"
"github.com/polymorfa/hypermeow/proto/waCommon"
"github.com/polymorfa/hypermeow/proto/waServerSync"
"github.com/polymorfa/hypermeow/proto/waSyncAction"
"github.com/polymorfa/hypermeow/types"
"github.com/polymorfa/hypermeow/util/cbcutil"
)

// MutationInfo contains information about a single mutation to the app state.
Expand Down Expand Up @@ -162,6 +162,26 @@ func BuildLabelChat(target types.JID, labelID string, labeled bool) PatchInfo {
}
}

type LabelChatChange struct {
LabelID string
Labeled bool
}

func BuildLabelChatChanges(target types.JID, changes []LabelChatChange) PatchInfo {
mutations := make([]MutationInfo, 0, len(changes))
mutationIndexes := make(map[string]int, len(changes))
for _, change := range changes {
mutation := newLabelChatMutation(target, change.LabelID, change.Labeled)
if index, exists := mutationIndexes[change.LabelID]; exists {
mutations[index] = mutation
} else {
mutationIndexes[change.LabelID] = len(mutations)
mutations = append(mutations, mutation)
}
}
return PatchInfo{Type: WAPatchRegular, Mutations: mutations}
}

func newLabelMessageMutation(target types.JID, labelID, messageID string, labeled bool) MutationInfo {
return MutationInfo{
Index: []string{IndexLabelAssociationMessage, labelID, target.String(), messageID, "0", "0"},
Expand Down Expand Up @@ -208,6 +228,43 @@ func BuildLabelEdit(labelID string, labelName string, labelColor int32, deleted
}
}

func newQuickReplyMutation(id, shortcut, message string, keywords []string, count int32, deleted bool, associatedLabelIDs []string) MutationInfo {
return MutationInfo{
Index: []string{IndexQuickReply, id},
Version: 2,
Value: &waSyncAction.SyncActionValue{
QuickReplyAction: &waSyncAction.QuickReplyAction{
Shortcut: proto.String(shortcut),
Message: proto.String(message),
Keywords: keywords,
Count: proto.Int32(count),
Deleted: proto.Bool(deleted),
AssociatedLabelIDs: associatedLabelIDs,
},
},
}
}

// BuildQuickReply builds an app state patch for adding or editing a quick reply.
func BuildQuickReply(id, shortcut, message string, keywords []string, count int32, associatedLabelIDs ...string) PatchInfo {
return PatchInfo{
Type: WAPatchRegular,
Mutations: []MutationInfo{
newQuickReplyMutation(id, shortcut, message, keywords, count, false, associatedLabelIDs),
},
}
}

// BuildQuickReplyDelete builds an app state tombstone for deleting a quick reply.
func BuildQuickReplyDelete(id string) PatchInfo {
return PatchInfo{
Type: WAPatchRegular,
Mutations: []MutationInfo{
newQuickReplyMutation(id, "", "", []string{}, 0, true, nil),
},
}
}

func newSettingPushNameMutation(pushName string) MutationInfo {
return MutationInfo{
Index: []string{IndexSettingPushName},
Expand Down
45 changes: 45 additions & 0 deletions appstate/encode_label_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package appstate

import (
"testing"

"github.com/polymorfa/hypermeow/types"
)

func TestBuildLabelChatChangesUsesOnePatch(t *testing.T) {
target := types.NewJID("15551234567", types.DefaultUserServer)
patch := BuildLabelChatChanges(target, []LabelChatChange{
{LabelID: "1", Labeled: false},
{LabelID: "2", Labeled: true},
})
if patch.Type != WAPatchRegular || len(patch.Mutations) != 2 {
t.Fatalf("patch = %#v", patch)
}
for index, want := range []struct {
labelID string
labeled bool
}{{"1", false}, {"2", true}} {
mutation := patch.Mutations[index]
if mutation.Version != 3 || len(mutation.Index) != 3 || mutation.Index[1] != want.labelID || mutation.Index[2] != target.String() {
t.Fatalf("mutation %d = %#v", index, mutation)
}
if mutation.Value.GetLabelAssociationAction().GetLabeled() != want.labeled {
t.Fatalf("mutation %d labeled = %v", index, mutation.Value.GetLabelAssociationAction().GetLabeled())
}
}
}

func TestBuildLabelChatChangesDeduplicatesLabelIDs(t *testing.T) {
target := types.NewJID("15551234567", types.DefaultUserServer)
patch := BuildLabelChatChanges(target, []LabelChatChange{
{LabelID: "1", Labeled: false},
{LabelID: "2", Labeled: true},
{LabelID: "1", Labeled: true},
})
if len(patch.Mutations) != 2 {
t.Fatalf("mutation count = %d, want 2", len(patch.Mutations))
}
if patch.Mutations[0].Index[1] != "1" || !patch.Mutations[0].Value.GetLabelAssociationAction().GetLabeled() {
t.Fatalf("duplicate label did not preserve its final state: %#v", patch.Mutations[0])
}
}
Loading
Loading