Skip to content

feat(events): forward Picture, UserAbout and BusinessName webhook events#57

Open
walterdiazesa wants to merge 1 commit intoevolution-foundation:mainfrom
walterdiazesa:feat/forward-picture-userabout-businessname-events
Open

feat(events): forward Picture, UserAbout and BusinessName webhook events#57
walterdiazesa wants to merge 1 commit intoevolution-foundation:mainfrom
walterdiazesa:feat/forward-picture-userabout-businessname-events

Conversation

@walterdiazesa
Copy link
Copy Markdown

@walterdiazesa walterdiazesa commented May 9, 2026

Description

Three whatsmeow events that the underlying go.mau.fi/whatsmeow library emits are silently dropped by the dispatcher in pkg/whatsmeow/service/whatsmeow.go because the central switch has no case for them. They show up in the binary's stdout as Unhandled event *events.X and 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

whatsmeow event Source Fires when
*events.Picture whatsmeow/types/events/events.go Any user's profile picture or any group's photo changes. Fields: JID, Author, Timestamp, Remove, PictureID.
*events.UserAbout whatsmeow/types/events/events.go Any user's "about" / status text changes. Fields: JID, Status, Timestamp.
*events.BusinessName whatsmeow/types/events/appstate.go Lazily on inbound messages whose verified business name differs from the cached one. Fields: JID, OldBusinessName, NewBusinessName.

What the changes do

  1. pkg/whatsmeow/service/whatsmeow.gomyEventHandler switch: three new cases mirroring the minimal style of the existing *events.Contact / *events.PushName handlers (set doWebhook = true and postMap["event"]; the raw event is already attached at line 850 via postMap["data"] = rawEvt).

  2. pkg/whatsmeow/service/whatsmeow.goCallWebhook event-name switch: three new branches so consumers can subscribe to PICTURE / USER_ABOUT / BUSINESS_NAME independently. Followed the existing per-category split pattern (e.g. MESSAGE vs SEND_MESSAGE vs READ_RECEIPT) rather than folding into CONTACT. Happy to adjust if the maintainers prefer folding.

  3. pkg/internal/event_types/event_types.go: three new constants in the const block + AllEventTypes slice + 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).

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. A Picture event with JID + PictureID lets 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 the Connected re-emit side-effect (which only carries the user's own pushName, 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:

[WARN] Unhandled event *events.Picture: &{JID:162912521969702@lid Author: 
  Timestamp:2026-05-08 23:03:24 -0300 -03 Remove:false PictureID:1950407268}

with no corresponding webhook delivery. After this PR, the same scenario delivers a Picture webhook event to consumers that include PICTURE (or ALL) 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

  • Bug fix (non-breaking change which fixes an issue)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • Documentation update
  • Refactoring (no functional changes)
  • Performance improvement

Testing

  • Manual testing completed
  • Functionality verified in development environment
  • No breaking changes introduced

How I tested:

  1. Built the patched binary with go build ./... against the pinned whatsmeow-lib submodule (0923702fb3fac8525241f15331b92116485d69eb). Build succeeded.
  2. Verified the three new constants are exported from event_types and accepted by IsEventType (the validator the subscribe filter uses at three call sites in whatsmeow.go).
  3. Verified the dispatch path: each new case sets doWebhook = true so the existing if doWebhook { ... } block at line 1944 fires, attaching instanceToken / instanceId / instanceName and routing through CallWebhook to 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/ or pkg/internal/event_types/, only in pkg/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

  • My code follows the project's style guidelines
  • I have performed a self-review of my code
  • I have tested my changes thoroughly
  • Any dependent changes have been merged and published

Additional Notes

A couple of small design questions I'm happy to iterate on in review:

  1. Subscribe enum granularity: I went with three separate categories (PICTURE, USER_ABOUT, BUSINESS_NAME) following the existing fine-grained split (e.g. MESSAGE vs SEND_MESSAGE vs READ_RECEIPT). Folding into CONTACT would be smaller but loses opt-in granularity — let me know if you prefer that direction.
  2. Payload shape: the new cases use the flat style postMap["event"] = "X" (matching *events.Contact, *events.PushName, *events.GroupInfo, etc.). The raw event lands in postMap["data"] automatically thanks to line 850. If you'd prefer an explicit field projection here (matching what *events.Receipt does for read receipts), I can update.

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).
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented May 9, 2026

Reviewer's Guide

Adds 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 events

sequenceDiagram
    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
Loading

Class diagram for whatsmeow event routing and new event types

classDiagram
    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
Loading

File-Level Changes

Change Details Files
Forward Picture, UserAbout, and BusinessName whatsmeow events from the client event handler to the webhook pipeline.
  • Extend myEventHandler switch to handle Picture, UserAbout, and BusinessName events.
  • Mark these events for webhook dispatch and set the corresponding event name in the outgoing payload.
pkg/whatsmeow/service/whatsmeow.go
Enable subscription-based routing for Picture, UserAbout, and BusinessName webhook events.
  • Add new CallWebhook switch branches for Picture, UserAbout, and BusinessName event types.
  • Gate dispatch of each new event type on matching subscription keys PICTURE, USER_ABOUT, and BUSINESS_NAME, logging and forwarding when enabled.
pkg/whatsmeow/service/whatsmeow.go
Register PICTURE, USER_ABOUT, and BUSINESS_NAME as valid event types for subscription validation.
  • Introduce new event type constants for PICTURE, USER_ABOUT, and BUSINESS_NAME.
  • Add the new event types to the AllEventTypes slice so they are exposed to consumers.
  • Mark the new event types as valid in the validEventTypes map so IsEventType accepts them.
pkg/internal/event_types/event_types.go

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • 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.
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.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@walterdiazesa
Copy link
Copy Markdown
Author

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 myEventHandler setting postMap["event"] = "X" with the literal string, paired with 18 case "X": blocks in CallWebhook checking the corresponding subscribe category. The constants in event_types.go don't currently feed any of those — every existing event is also written as a literal in both places. Centralizing would require touching all ~50 string literals across both functions in one go.

On the repeated contains/log/send pattern in CallWebhook: same story. 18 existing cases follow the exact same shape:

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 dispatchIfSubscribed helper would absolutely clean this up, but the benefit only materializes when applied to all 18 cases — applying it only to the 3 new ones would leave the codebase inconsistent.

Both are valid follow-up improvements, but I'd argue they belong in a separate refactor PR for two reasons:

  1. Scope: this PR is purely additive (+33 lines, 0 deletions, no behavior changes to existing events). A refactor that touches 50+ string literals and 18 case bodies has a much wider blast radius and is harder to review safely.
  2. Consistency: maintainers may prefer the existing convention as intentional (it's verbose but every event is locally readable — case "X": makes it obvious which subscription gate it falls under, without jumping to a helper definition). Mixing the new style only for the 3 new events leaves the codebase split.

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant