feat(webhook): smart pre-dispatch filters + WaId-aware contact matching#2
Open
tobiasstrebitzer wants to merge 5 commits into
Open
feat(webhook): smart pre-dispatch filters + WaId-aware contact matching#2tobiasstrebitzer wants to merge 5 commits into
tobiasstrebitzer wants to merge 5 commits into
Conversation
…essage from-filter Implements rmyndharis#349 (the typed-WaId + resolution-table follow-up to rmyndharis#342). - WaId value object (src/engine/identity/wa-id.value.ts): in-memory only, serializes byte-identically to today's neutral JID; three-valued refersToSamePerson. - lid_mappings table on the data connection (global, last-write-wins, nullable phone for a negative cache) + portable pg/sqlite migration; LidMappingStore loads on boot and writes through, keeping resolvePhone synchronous. - Back resolvePhone with the table; populate it at runtime from the lid<->phone pairs the Baileys engine actually carries: inbound senderPn/participantPn, chats.phoneNumberShare, contacts (jid), and history sync. - from-filter on GET messages that resolves a phone to its lids, so a lid-resolved match becomes a hit (closes the silent-miss gap); covered by a test. - On-demand POST :chatId/history/sync (Baileys fetchMessageHistory) to backfill mappings on an already-authed session. - Canonicalize Baileys contact/chat listing ids to @c.us (read-back folds the neutral id back to the engine dialect so send/mark-read round-trip). RESOLVE_LID_TO_PHONE resolves internally into the table and gates only what is exposed (privacy flag, not a correctness toggle), per maintainer guidance on rmyndharis#349.
…debug logging Downstream Baileys-engine hardening, stacked on the typed-WaId work (rmyndharis#349). - Initial sync: pass shouldSyncHistoryMessage: () => true. Baileys defaults it to () => !!syncFullHistory, so with syncFullHistory unset it silently disabled the entire initial sync - no contacts, chat list, recent messages, or lid->phone mappings ever arrived. The full-archive download stays opt-in via BAILEYS_SYNC_FULL_HISTORY. - Message history chatId filter resolves across dialects (reuses the from-filter's resolveJidCandidates), so a chat keyed @c.us also matches messages stored under @s.whatsapp.net - fixes the empty conversation view after contact/chat canonicalization. - BAILEYS_LOG_LEVEL surfaces the Baileys library's own logs (trace dumps the decoded WA wire frames), plus contacts/chats/history-set receipt instrumentation, for diagnosing sync issues. Silent by default.
The descendant selector `.modal-body label` leaked into nested labels (e.g. the filter builder's inputs), restyling controls it shouldn't. Scope it to `.modal-body > label` so only top-level modal field labels are affected.
Add optional smart filters that refine WHETHER a subscribed webhook fires, evaluated per event before delivery. Conditions (AND-combined) match on sender/recipient/body/type/mentions/fromMe/hasMedia/isGroup with is/isNot/contains/equals/matches operators; message-only conditions are skipped for non-message events, so a webhook subscribed to several families behaves sanely without per-event filter sets. A webhook with no filters behaves exactly as before (additive, zero-config). - filters/: family-aware field registry, evaluator, class-validator validation - entity column + DTO field + migration for the persisted filters - wire evaluateFilters into dispatch matching - dashboard FilterBuilder UI on the webhooks page (sender picker backed by a session-chats query) + i18n strings - unit coverage for the evaluator/validation and dispatch, plus a webhooks e2e suite
Match id filters (sender/recipient/mentions) by the engine-neutral WaId key instead of the raw JID, reusing the same identity primitives as the engine and MessageService. An engine-emitted id (any of @c.us / @s.whatsapp.net / @lid, optional :device suffix, a lid resolved to its phone) and a user-typed filter value (bare digits or a JID) both collapse to one neutral key, so: - a phone filter matches the same person across user dialects, and - a lid-addressed actor (e.g. an unresolved @lid group participant) now matches a phone filter once the persistent lid->phone table knows the mapping - previously a silent miss. WebhookService takes the cross-session LidMappingStore (optional) and threads a resolver into evaluateFilters at dispatch; the evaluator stays a pure function (resolver optional) so unit contexts need no store. No new filter fields, no UI change - the dashboard keeps storing the neutral <digits>@c.us value.
50ccff7 to
b5961fa
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Smart webhook filters (+ WaId-aware contact matching)
Optional, additive pre-dispatch filters for webhook triggers. A webhook with no filters behaves exactly as before (zero extra clicks, zero behaviour change). When filters are present, every condition must match (AND) for the webhook to fire.
Why
Webhook triggers fire on event type only. There are legitimate reasons to pre-filter before delivery (Gmail-forwarding-style rules): only fire when sender = A, body contains "invoice", it is a group, and so on. This cuts webhook congestion and over-emitting and, in the age of AI, controls token/cost downstream.
message.*is the natural start; the design is event-family aware sosession/groupcan slot in later without touching the evaluator.A second motivation is correctness of who a filter matches. WhatsApp addresses the same person through several dialects (
@c.us,@s.whatsapp.net,@lid), and a privacy-id (@lid) sender's number is not a phone at all. A naive JID compare silently misses the same contact across dialects, and misses a lid-addressed sender entirely. This resolves ids through the typed-WaIdlayer + the persistentlid -> phonetable from rmyndharis#374, so a phone filter matches the person everywhere.What ships
sender,recipient,body,type,mentions,isGroup,fromMe,hasMedia. Operators:is/isNot(ids, enums),contains/equals/matches(text, regex), optionalcaseSensitive. Message-only conditions are skipped on non-message events, so a*-subscribed webhook still fires on session events.sender/recipient/mentionsmatch by the neutralWaIdkey, not the raw JID. An engine-emitted id (any dialect, optional:device, a lid the table resolves to its phone) and a user-typed value (bare digits or a JID) collapse to one neutral key, so a phone filter matches across@c.us/@s.whatsapp.netand any@lidresolving to that phone - previously a silent miss.filtersJSON column onwebhooks(portable migration:jsonbon Postgres,texton SQLite, idempotent guard). Exposed asfilterson create/update DTOs and the response.Commits
fix(dashboard): scope .modal-body label to direct children- unrelated CSS fix carried along.feat(webhook): smart pre-dispatch filters- filters subsystem (family-aware field registry, evaluator, class-validator validation), entity column + DTO + migration, dispatch wiring, FilterBuilder UI + badge popover + i18n, unit coverage + a new webhooks e2e suite.feat(webhook): WaId-aware id matching via the lid->phone table- replaces raw-JID compare withWaId-neutral keys; injects the cross-sessionLidMappingStoreintoWebhookServiceand threads a lid resolver into the evaluator (which stays a pure function, resolver optional).Design notes
main(scopedfindOne, SSRF IP-pin viawithSafeFetch, per-occurrence idempotency). That refactor commit was dropped as redundant; filters layer onto main's structure, preserving the SSRF pin.WaId.fromEngineJid/fromUserInput/.toNeutral(); the dispatch resolver mirrors the adapter'sresolvePhone, backed byLidMappingStore.getCached.WebhookModuleimportsEngineModule(exportsLidMappingStoreService); the store is@Optional()-injected so unit/queue contexts work without it. The fullAppModuleboots in the e2e suite (confirms no circular dep).<digits>@c.usvalue; all dialect/lid normalisation is server-side.Testing
npm run build,npm run lint: clean.npm test: 1010 unit tests. New coverage includes cross-dialect / bare-number /:devicematching and a lid-resolution hit + no-resolver miss control (evaluator and dispatch level).npm run test:e2e: 26 (incl. the newwebhooks.e2e-spec.ts: CRUD, auth, SSRF reject, HMAC on the wire, header sanitisation, filter behaviour).tsc -b,eslint src,vite buildclean.CHANGELOG [Unreleased]entry added.