feat(events): forward Picture, UserAbout and BusinessName webhook events#57
Conversation
Three whatsmeow events were silently dropped by the dispatcher
("Unhandled event" log) because the central switch in
pkg/whatsmeow/service/whatsmeow.go had no case for them, even though
the underlying go.mau.fi/whatsmeow library emits them:
- *events.Picture (types/events/events.go) — fires when any user's
profile picture or any group's photo changes. Carries JID, Author,
Timestamp, Remove, PictureID. Useful for keeping CRM avatar caches
in sync without polling /chat/fetchProfilePictureUrl per contact.
- *events.UserAbout (types/events/events.go) — fires when any user's
"about" / status text changes. Carries JID, Status, Timestamp.
- *events.BusinessName (types/events/appstate.go) — fires lazily on
inbound messages whose verified business name differs from the
cached one. Carries JID, OldBusinessName, NewBusinessName.
Changes:
1. pkg/whatsmeow/service/whatsmeow.go — added three new cases to the
myEventHandler dispatch switch, mirroring the minimal style of the
existing *events.Contact / *events.PushName handlers (set doWebhook
and postMap["event"]; the raw event is already attached at line 850
via postMap["data"] = rawEvt).
2. pkg/whatsmeow/service/whatsmeow.go (CallWebhook) — added three new
case branches on eventType so consumers can subscribe to PICTURE /
USER_ABOUT / BUSINESS_NAME independently. Following the existing
per-category split (e.g. MESSAGE vs SEND_MESSAGE vs READ_RECEIPT)
rather than folding into CONTACT.
3. pkg/internal/event_types/event_types.go — added the three new
subscription category constants to the const block, AllEventTypes
slice, and validEventTypes map so IsEventType accepts them.
The change is purely additive: existing subscribers keep receiving
exactly what they did before. Consumers wanting the new events must
opt in by including PICTURE / USER_ABOUT / BUSINESS_NAME in their
subscribe list (or use ALL).
No new dependencies, no behavior changes to existing events. Build
verified with `go build ./...` against the pinned whatsmeow-lib
submodule (0923702fb3fac8525241f15331b92116485d69eb).
Reviewer's GuideAdds support for forwarding three previously unhandled whatsmeow events (Picture, UserAbout, BusinessName) through the webhook/queue system by wiring them into the event handler switch, subscription routing, and event-type validation. Sequence diagram for forwarding new Picture/UserAbout/BusinessName eventssequenceDiagram
actor WhatsAppUser
participant WhatsAppServer
participant WhatsmeowLibrary
participant MyClient
participant whatsmeowService
participant WebhookConsumer
WhatsAppUser->>WhatsAppServer: Change profile picture / about / business name
WhatsAppServer-->>WhatsmeowLibrary: Emit events.Picture / events.UserAbout / events.BusinessName
WhatsmeowLibrary-->>MyClient: rawEvt
MyClient->>MyClient: myEventHandler(rawEvt)
alt events.Picture
MyClient->>MyClient: doWebhook = true
MyClient->>MyClient: postMap[event] = Picture
else events.UserAbout
MyClient->>MyClient: doWebhook = true
MyClient->>MyClient: postMap[event] = UserAbout
else events.BusinessName
MyClient->>MyClient: doWebhook = true
MyClient->>MyClient: postMap[event] = BusinessName
end
MyClient->>whatsmeowService: CallWebhook(instance, queueName, jsonData, eventType)
alt eventType == Picture and subscriptions contain PICTURE
whatsmeowService->>whatsmeowService: log Event received of type Picture
whatsmeowService->>WebhookConsumer: sendToQueueOrWebhook(instance, queueName, jsonData)
else eventType == UserAbout and subscriptions contain USER_ABOUT
whatsmeowService->>whatsmeowService: log Event received of type UserAbout
whatsmeowService->>WebhookConsumer: sendToQueueOrWebhook(instance, queueName, jsonData)
else eventType == BusinessName and subscriptions contain BUSINESS_NAME
whatsmeowService->>whatsmeowService: log Event received of type BusinessName
whatsmeowService->>WebhookConsumer: sendToQueueOrWebhook(instance, queueName, jsonData)
else Not subscribed
whatsmeowService--xWebhookConsumer: Event dropped
end
Class diagram for whatsmeow event routing and new event typesclassDiagram
class MyClient {
+myEventHandler(rawEvt interface)
-bool doWebhook
-map<string, interface> postMap
}
class whatsmeowService {
+CallWebhook(instance *Instance, queueName string, jsonData []byte, eventType string, subscriptions []string)
-loggerWrapper LoggerWrapper
+sendToQueueOrWebhook(instance *Instance, queueName string, jsonData []byte)
}
class EventTypes {
<<enumeration>>
+const MESSAGE string
+const SEND_MESSAGE string
+const READ_RECEIPT string
+const NEWSLETTER string
+const QRCODE string
+const BUTTON_CLICK string
+const PICTURE string
+const USER_ABOUT string
+const BUSINESS_NAME string
+var AllEventTypes []string
+var validEventTypes map<string, bool>
+IsEventType(eventType string) bool
}
class Instance {
+Id string
+Token string
+Name string
}
class LoggerWrapper {
+GetLogger(instanceId string) Logger
}
class Logger {
+LogInfo(format string, args ...interface)
}
MyClient --> whatsmeowService : uses
whatsmeowService --> Instance : uses
whatsmeowService --> LoggerWrapper : uses
LoggerWrapper --> Logger : returns
whatsmeowService --> EventTypes : checks subscriptions against constants
EventTypes ..> Instance : events delivered via subscription list
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
There was a problem hiding this comment.
Hey - I've left some high level feedback:
- The event names are now duplicated as string literals across
myEventHandler,CallWebhook, andevent_types; consider centralizing these as shared constants (or deriving one from the other) to avoid future drift or typos when renaming/adding events. - The
CallWebhookswitch cases for the new events repeat the samecontains/log/send pattern as many existing cases; consider extracting a small helper (e.g.dispatchIfSubscribed(eventType, subscriptionKey, ...)) to reduce repetition and make it harder to miswire future event types.
Prompt for AI Agents
Please address the comments from this code review:
## Overall Comments
- The event names are now duplicated as string literals across `myEventHandler`, `CallWebhook`, and `event_types`; consider centralizing these as shared constants (or deriving one from the other) to avoid future drift or typos when renaming/adding events.
- The `CallWebhook` switch cases for the new events repeat the same `contains`/log/send pattern as many existing cases; consider extracting a small helper (e.g. `dispatchIfSubscribed(eventType, subscriptionKey, ...)`) to reduce repetition and make it harder to miswire future event types.Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.
|
Thanks for the review @sourcery-ai (re: review #4256506860). Both observations are technically correct, but I'd push back on addressing them in this PR — let me explain. On the duplicated event-name strings: the same pattern is pre-existing across the entire dispatcher. There are 31 case blocks in On the repeated case "X":
if contains(subscriptions, "Y") {
w.loggerWrapper.GetLogger(instance.Id).LogInfo("[%s] Event received of type %s", instance.Id, eventType)
w.sendToQueueOrWebhook(instance, queueName, jsonData)
}A Both are valid follow-up improvements, but I'd argue they belong in a separate refactor PR for two reasons:
Happy to open a follow-up refactor PR after this one merges if maintainers want the cleanup applied uniformly. Or, if you'd prefer it done in this PR, let me know which direction (centralized constants only, helper extraction only, or both). For this PR, I'd lean toward landing it as-is to keep the surface narrow, then iterate on the refactor separately. |
Description
Three whatsmeow events that the underlying
go.mau.fi/whatsmeowlibrary emits are silently dropped by the dispatcher inpkg/whatsmeow/service/whatsmeow.gobecause the central switch has no case for them. They show up in the binary's stdout asUnhandled event *events.Xand are never forwarded.This PR adds the three missing handlers + the matching subscribe categories so consumers can receive them via webhook / queue / websocket.
The three events
*events.Picturewhatsmeow/types/events/events.goJID,Author,Timestamp,Remove,PictureID.*events.UserAboutwhatsmeow/types/events/events.goJID,Status,Timestamp.*events.BusinessNamewhatsmeow/types/events/appstate.goJID,OldBusinessName,NewBusinessName.What the changes do
pkg/whatsmeow/service/whatsmeow.go—myEventHandlerswitch: three new cases mirroring the minimal style of the existing*events.Contact/*events.PushNamehandlers (setdoWebhook = trueandpostMap["event"]; the raw event is already attached at line 850 viapostMap["data"] = rawEvt).pkg/whatsmeow/service/whatsmeow.go—CallWebhookevent-name switch: three new branches so consumers can subscribe toPICTURE/USER_ABOUT/BUSINESS_NAMEindependently. Followed the existing per-category split pattern (e.g.MESSAGEvsSEND_MESSAGEvsREAD_RECEIPT) rather than folding intoCONTACT. Happy to adjust if the maintainers prefer folding.pkg/internal/event_types/event_types.go: three new constants in the const block +AllEventTypesslice +validEventTypesmap soIsEventTypeaccepts them.The change is purely additive: existing subscribers keep receiving exactly what they did before. Consumers wanting the new events must opt in by including
PICTURE/USER_ABOUT/BUSINESS_NAMEin their subscribe list (or useALL).Why this matters (use cases)
Picture: keeping a CRM's avatar cache in sync with the live WhatsApp account. Today the only way is to call/chat/fetchProfilePictureUrl/{instance}per contact, which has no batch endpoint and risks rate-flagging when sweeping hundreds of contacts. APictureevent withJID+PictureIDlets consumers invalidate their cache real-time without polling.UserAbout: surfacing each contact's status text in CRM contact cards / chat lists. Currently impossible to track without polling.BusinessName: showing the contact's verified business name correctly in the CRM after they edit it on their phone. Today the only way to detect a business-name change is theConnectedre-emit side-effect (which only carries the user's ownpushName, not the verified business name field), so contact-side renames are completely invisible to consumers.Reproduction of the original bug (before this PR)
Subscribe an instance to
["ALL"], pair via QR, then change a contact's profile picture from your phone. The binary stdout shows:with no corresponding webhook delivery. After this PR, the same scenario delivers a
Picturewebhook event to consumers that includePICTURE(orALL) in their subscribe list.Related Issue
This PR opens directly with the fix; happy to also file a tracking issue if the maintainers prefer that workflow.
Type of Change
Testing
How I tested:
go build ./...against the pinnedwhatsmeow-libsubmodule (0923702fb3fac8525241f15331b92116485d69eb). Build succeeded.event_typesand accepted byIsEventType(the validator the subscribe filter uses at three call sites inwhatsmeow.go).casesetsdoWebhook = trueso the existingif doWebhook { ... }block at line 1944 fires, attachinginstanceToken/instanceId/instanceNameand routing throughCallWebhookto the configured webhook URL.I'd be happy to add unit tests if the project has a preferred location / pattern for them — I didn't see existing tests in
pkg/whatsmeow/service/orpkg/internal/event_types/, only inpkg/utils/utils_test.go, so I followed the existing conventions and added none. Let me know if you want me to add coverage as part of this PR.Screenshots (if applicable)
N/A — server-side dispatcher change.
Checklist
Additional Notes
A couple of small design questions I'm happy to iterate on in review:
PICTURE,USER_ABOUT,BUSINESS_NAME) following the existing fine-grained split (e.g.MESSAGEvsSEND_MESSAGEvsREAD_RECEIPT). Folding intoCONTACTwould be smaller but loses opt-in granularity — let me know if you prefer that direction.postMap["event"] = "X"(matching*events.Contact,*events.PushName,*events.GroupInfo, etc.). The raw event lands inpostMap["data"]automatically thanks to line 850. If you'd prefer an explicit field projection here (matching what*events.Receiptdoes for read receipts), I can update.