Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
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 src/domains/app/app.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ type IAppUsecase interface {
type DevicesResponse struct {
Name string `json:"name"`
Device string `json:"device"`
JID string `json:"jid,omitempty"`
}

type LoginResponse struct {
Expand Down
13 changes: 8 additions & 5 deletions src/infrastructure/whatsapp/event_call.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
)

// handleCallOffer handles incoming call events and optionally auto-rejects them
func handleCallOffer(ctx context.Context, evt *events.CallOffer, chatStorageRepo domainChatStorage.IChatStorageRepository, deviceID string, client *whatsmeow.Client) {
func handleCallOffer(ctx context.Context, evt *events.CallOffer, chatStorageRepo domainChatStorage.IChatStorageRepository, sessionID string, deviceID string, client *whatsmeow.Client) {
logrus.Infof("Incoming call from %s (CallID: %s)", evt.CallCreator.String(), evt.CallID)

// Auto-reject call if configured
Expand Down Expand Up @@ -47,15 +47,15 @@ func handleCallOffer(ctx context.Context, evt *events.CallOffer, chatStorageRepo
go func(e *events.CallOffer, c *whatsmeow.Client, rejected bool) {
webhookCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := forwardCallOfferToWebhook(webhookCtx, e, deviceID, c, rejected); err != nil {
if err := forwardCallOfferToWebhook(webhookCtx, e, sessionID, deviceID, c, rejected); err != nil {
logrus.Errorf("Failed to forward call event to webhook: %v", err)
}
}(evt, client, autoRejected)
}
}

// createCallOfferPayload creates a webhook payload for incoming call events
func createCallOfferPayload(ctx context.Context, evt *events.CallOffer, deviceID string, client *whatsmeow.Client, autoRejected bool) map[string]any {
func createCallOfferPayload(ctx context.Context, evt *events.CallOffer, sessionID string, deviceID string, client *whatsmeow.Client, autoRejected bool) map[string]any {
body := make(map[string]any)
payload := make(map[string]any)

Expand Down Expand Up @@ -83,13 +83,16 @@ func createCallOfferPayload(ctx context.Context, evt *events.CallOffer, deviceID
if deviceID != "" {
body["device_id"] = deviceID
}
if sessionID != "" {
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Suggestion: This if sessionID != "" { body["session_id"] = sessionID } pattern is repeated across 9+ webhook functions (call, delete, group, joined-group, newsletter join/leave/update/mute, receipt). Consider extracting a helper:

func buildWebhookEnvelope(sessionID, deviceID string) map[string]any {
    body := make(map[string]any)
    if deviceID != "" {
        body["device_id"] = deviceID
    }
    if sessionID != "" {
        body["session_id"] = sessionID
    }
    return body
}

This reduces duplication and ensures any future envelope changes are applied uniformly.

body["session_id"] = sessionID
}
body["payload"] = payload

return body
}

// forwardCallOfferToWebhook forwards incoming call events to the configured webhook URLs
func forwardCallOfferToWebhook(ctx context.Context, evt *events.CallOffer, deviceID string, client *whatsmeow.Client, autoRejected bool) error {
payload := createCallOfferPayload(ctx, evt, deviceID, client, autoRejected)
func forwardCallOfferToWebhook(ctx context.Context, evt *events.CallOffer, sessionID string, deviceID string, client *whatsmeow.Client, autoRejected bool) error {
payload := createCallOfferPayload(ctx, evt, sessionID, deviceID, client, autoRejected)
return forwardPayloadToConfiguredWebhooks(ctx, payload, "call.offer")
}
9 changes: 6 additions & 3 deletions src/infrastructure/whatsapp/event_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,8 @@ import (
)

// forwardDeleteToWebhook sends a delete event to webhook
func forwardDeleteToWebhook(ctx context.Context, evt *events.DeleteForMe, message *domainChatStorage.Message, deviceID string, client *whatsmeow.Client) error {
payload, err := createDeletePayload(ctx, evt, message, deviceID, client)
func forwardDeleteToWebhook(ctx context.Context, evt *events.DeleteForMe, message *domainChatStorage.Message, sessionID string, deviceID string, client *whatsmeow.Client) error {
payload, err := createDeletePayload(ctx, evt, message, sessionID, deviceID, client)
if err != nil {
return err
}
Expand All @@ -20,7 +20,7 @@ func forwardDeleteToWebhook(ctx context.Context, evt *events.DeleteForMe, messag
}

// createDeletePayload creates a webhook payload for delete events
func createDeletePayload(ctx context.Context, evt *events.DeleteForMe, message *domainChatStorage.Message, deviceID string, client *whatsmeow.Client) (map[string]any, error) {
func createDeletePayload(ctx context.Context, evt *events.DeleteForMe, message *domainChatStorage.Message, sessionID string, deviceID string, client *whatsmeow.Client) (map[string]any, error) {
body := make(map[string]any)
payload := make(map[string]any)

Expand Down Expand Up @@ -49,6 +49,9 @@ func createDeletePayload(ctx context.Context, evt *events.DeleteForMe, message *
if deviceID != "" {
body["device_id"] = deviceID
}
if sessionID != "" {
body["session_id"] = sessionID
}
body["payload"] = payload

return body, nil
Expand Down
18 changes: 12 additions & 6 deletions src/infrastructure/whatsapp/event_group.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import (
)

// createGroupInfoPayload creates a webhook payload for group information events
func createGroupInfoPayload(ctx context.Context, evt *events.GroupInfo, actionType string, jids []types.JID, deviceID string, client *whatsmeow.Client) map[string]any {
func createGroupInfoPayload(ctx context.Context, evt *events.GroupInfo, actionType string, jids []types.JID, sessionID string, deviceID string, client *whatsmeow.Client) map[string]any {
body := make(map[string]any)

// Create payload structure matching the expected format
Expand All @@ -35,6 +35,9 @@ func createGroupInfoPayload(ctx context.Context, evt *events.GroupInfo, actionTy
if deviceID != "" {
body["device_id"] = deviceID
}
if sessionID != "" {
body["session_id"] = sessionID
}

return body
}
Expand All @@ -55,7 +58,7 @@ func jidsToStrings(ctx context.Context, jids []types.JID, client *whatsmeow.Clie
}

// forwardGroupInfoToWebhook forwards group information events to the configured webhook URLs
func forwardGroupInfoToWebhook(ctx context.Context, evt *events.GroupInfo, deviceID string, client *whatsmeow.Client) error {
func forwardGroupInfoToWebhook(ctx context.Context, evt *events.GroupInfo, sessionID string, deviceID string, client *whatsmeow.Client) error {
// Send separate webhook events for each action type
actions := []struct {
actionType string
Expand All @@ -69,7 +72,7 @@ func forwardGroupInfoToWebhook(ctx context.Context, evt *events.GroupInfo, devic

for _, action := range actions {
if len(action.jids) > 0 {
payload := createGroupInfoPayload(ctx, evt, action.actionType, action.jids, deviceID, client)
payload := createGroupInfoPayload(ctx, evt, action.actionType, action.jids, sessionID, deviceID, client)

if err := forwardPayloadToConfiguredWebhooks(ctx, payload, "group.participants"); err != nil {
logrus.Warnf("Failed to forward group %s event to webhook: %v", action.actionType, err)
Expand All @@ -81,22 +84,22 @@ func forwardGroupInfoToWebhook(ctx context.Context, evt *events.GroupInfo, devic
}

// handleJoinedGroup handles the event when the connected device is added to a new group
func handleJoinedGroup(ctx context.Context, evt *events.JoinedGroup, deviceID string, client *whatsmeow.Client) {
func handleJoinedGroup(ctx context.Context, evt *events.JoinedGroup, sessionID string, deviceID string, client *whatsmeow.Client) {
log.Infof("Joined group %s (reason: %s, type: %s)", evt.JID, evt.Reason, evt.Type)

if len(config.WhatsappWebhook) > 0 {
go func(e *events.JoinedGroup, c *whatsmeow.Client) {
webhookCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := forwardJoinedGroupToWebhook(webhookCtx, e, deviceID, c); err != nil {
if err := forwardJoinedGroupToWebhook(webhookCtx, e, sessionID, deviceID, c); err != nil {
logrus.Errorf("Failed to forward joined group event to webhook: %v", err)
}
}(evt, client)
}
}

// forwardJoinedGroupToWebhook forwards the JoinedGroup event to configured webhooks
func forwardJoinedGroupToWebhook(ctx context.Context, evt *events.JoinedGroup, deviceID string, client *whatsmeow.Client) error {
func forwardJoinedGroupToWebhook(ctx context.Context, evt *events.JoinedGroup, sessionID string, deviceID string, client *whatsmeow.Client) error {
// Get own JID to include in the payload
ownJID := client.Store.ID
if ownJID == nil {
Expand All @@ -123,6 +126,9 @@ func forwardJoinedGroupToWebhook(ctx context.Context, evt *events.JoinedGroup, d
if deviceID != "" {
body["device_id"] = deviceID
}
if sessionID != "" {
body["session_id"] = sessionID
}

return forwardPayloadToConfiguredWebhooks(ctx, body, "group.joined")
}
35 changes: 19 additions & 16 deletions src/infrastructure/whatsapp/event_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@ func handler(ctx context.Context, instance *DeviceInstance, rawEvt any) {
chatStorageRepo := instance.GetChatStorage()
client := instance.GetClient()

sessionID := instance.ID()
deviceJID := instance.JID()

switch evt := rawEvt.(type) {
case *events.DeleteForMe:
handleDeleteForMe(ctx, evt, chatStorageRepo, instance.JID(), client)
handleDeleteForMe(ctx, evt, chatStorageRepo, sessionID, deviceJID, client)
case *events.AppStateSyncComplete:
handleAppStateSyncComplete(ctx, client, evt)
case *events.PairSuccess:
Expand All @@ -44,9 +47,9 @@ func handler(ctx context.Context, instance *DeviceInstance, rawEvt any) {
case *events.StreamReplaced:
handleStreamReplaced(ctx)
case *events.Message:
handleMessage(ctx, evt, chatStorageRepo, client)
handleMessage(ctx, evt, chatStorageRepo, sessionID, client)
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Inconsistency: handleMessage receives sessionID but not deviceJID, unlike every other event handler in this switch. The message webhook path re-derives device_id from client.Store.ID using NormalizeJIDFromLID() inside createWebhookEvent (event_message.go), while all other events use the pre-resolved deviceJID (from instance.JID()).

For LID-based WhatsApp accounts, NormalizeJIDFromLID() may resolve to a different value than instance.JID(), causing device_id to differ between message webhooks and all other webhook types.

Suggestion: Pass deviceJID to handleMessage as well, and have createWebhookEvent accept the pre-resolved JID instead of re-deriving it from the client store. This makes all event handlers use the same pattern.

case *events.Receipt:
handleReceipt(ctx, evt, instance.JID(), client)
handleReceipt(ctx, evt, sessionID, deviceJID, client)
case *events.Archive:
handleArchive(ctx, evt, chatStorageRepo, client)
case *events.Presence:
Expand All @@ -58,25 +61,25 @@ func handler(ctx context.Context, instance *DeviceInstance, rawEvt any) {
case *events.AppState:
handleAppState(ctx, evt)
case *events.GroupInfo:
handleGroupInfo(ctx, evt, instance.JID(), client)
handleGroupInfo(ctx, evt, sessionID, deviceJID, client)
case *events.JoinedGroup:
handleJoinedGroup(ctx, evt, instance.JID(), client)
handleJoinedGroup(ctx, evt, sessionID, deviceJID, client)
case *events.NewsletterJoin:
handleNewsletterJoin(ctx, evt, instance.JID(), client)
handleNewsletterJoin(ctx, evt, sessionID, deviceJID, client)
case *events.NewsletterLeave:
handleNewsletterLeave(ctx, evt, instance.JID(), client)
handleNewsletterLeave(ctx, evt, sessionID, deviceJID, client)
case *events.NewsletterLiveUpdate:
handleNewsletterLiveUpdate(ctx, evt, instance.JID(), client)
handleNewsletterLiveUpdate(ctx, evt, sessionID, deviceJID, client)
case *events.NewsletterMuteChange:
handleNewsletterMuteChange(ctx, evt, instance.JID(), client)
handleNewsletterMuteChange(ctx, evt, sessionID, deviceJID, client)
case *events.CallOffer:
handleCallOffer(ctx, evt, chatStorageRepo, instance.JID(), client)
handleCallOffer(ctx, evt, chatStorageRepo, sessionID, deviceJID, client)
}

instance.UpdateStateFromClient()
}

func handleDeleteForMe(ctx context.Context, evt *events.DeleteForMe, chatStorageRepo domainChatStorage.IChatStorageRepository, deviceID string, client *whatsmeow.Client) {
func handleDeleteForMe(ctx context.Context, evt *events.DeleteForMe, chatStorageRepo domainChatStorage.IChatStorageRepository, sessionID string, deviceID string, client *whatsmeow.Client) {
log.Infof("Deleted message %s for %s", evt.MessageID, evt.SenderJID.String())

// Find the message to get its chat JID
Expand All @@ -103,7 +106,7 @@ func handleDeleteForMe(ctx context.Context, evt *events.DeleteForMe, chatStorage
go func(c *whatsmeow.Client) {
webhookCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := forwardDeleteToWebhook(webhookCtx, evt, message, deviceID, c); err != nil {
if err := forwardDeleteToWebhook(webhookCtx, evt, message, sessionID, deviceID, c); err != nil {
Comment thread
occult marked this conversation as resolved.
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Inconsistency: sessionID, deviceID, evt, and message are captured from the outer scope rather than passed as goroutine function arguments. Compare with the message handler in event_message_handler.go which correctly passes all values as arguments:

go func(e *events.Message, c *whatsmeow.Client, sid string) {

While safe here (strings are immutable, pointers are not mutated after launch), the inconsistent pattern is fragile. Consider passing all referenced variables as goroutine arguments for uniformity.

log.Errorf("Failed to forward delete event to webhook: %v", err)
}
}(client)
Expand Down Expand Up @@ -214,7 +217,7 @@ func handleStreamReplaced(_ context.Context) {
os.Exit(0)
}

func handleReceipt(ctx context.Context, evt *events.Receipt, deviceID string, client *whatsmeow.Client) {
func handleReceipt(ctx context.Context, evt *events.Receipt, sessionID string, deviceID string, client *whatsmeow.Client) {
sendReceipt := false
switch evt.Type {
case types.ReceiptTypeRead, types.ReceiptTypeReadSelf:
Expand All @@ -231,7 +234,7 @@ func handleReceipt(ctx context.Context, evt *events.Receipt, deviceID string, cl
go func(e *events.Receipt, c *whatsmeow.Client) {
webhookCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := forwardReceiptToWebhook(webhookCtx, e, deviceID, c); err != nil {
if err := forwardReceiptToWebhook(webhookCtx, e, sessionID, deviceID, c); err != nil {
logrus.Errorf("Failed to forward ack event to webhook: %v", err)
}
}(evt, client)
Expand All @@ -254,7 +257,7 @@ func handleAppState(_ context.Context, evt *events.AppState) {
log.Debugf("App state event: %+v / %+v", evt.Index, evt.SyncActionValue)
}

func handleGroupInfo(ctx context.Context, evt *events.GroupInfo, deviceID string, client *whatsmeow.Client) {
func handleGroupInfo(ctx context.Context, evt *events.GroupInfo, sessionID string, deviceID string, client *whatsmeow.Client) {
// Only process events that have actual changes
hasChanges := len(evt.Join) > 0 || len(evt.Leave) > 0 || len(evt.Promote) > 0 || len(evt.Demote) > 0 ||
evt.Name != nil || evt.Topic != nil || evt.Locked != nil || evt.Announce != nil
Expand Down Expand Up @@ -282,7 +285,7 @@ func handleGroupInfo(ctx context.Context, evt *events.GroupInfo, deviceID string
go func(e *events.GroupInfo, c *whatsmeow.Client) {
webhookCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := forwardGroupInfoToWebhook(webhookCtx, e, deviceID, c); err != nil {
if err := forwardGroupInfoToWebhook(webhookCtx, e, sessionID, deviceID, c); err != nil {
logrus.Errorf("Failed to forward group info event to webhook: %v", err)
}
}(evt, client)
Expand Down
21 changes: 13 additions & 8 deletions src/infrastructure/whatsapp/event_message.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,15 @@ const (

// WebhookEvent is the top-level structure for webhook payloads
type WebhookEvent struct {
Event string `json:"event"`
DeviceID string `json:"device_id"`
Payload map[string]any `json:"payload"`
Event string `json:"event"`
DeviceID string `json:"device_id"`
SessionID string `json:"session_id,omitempty"`
Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Note: These JSON struct tags are decorative — WebhookEvent is never marshaled directly to JSON. Instead, forwardMessageToWebhook manually constructs a map[string]any from the struct fields. If someone later marshals the struct directly (relying on these tags), the behavior would differ from the current map-based approach.

Consider either marshaling the struct directly, or removing the JSON tags to avoid the false impression that they control serialization.

Payload map[string]any `json:"payload"`
}

// forwardMessageToWebhook is a helper function to forward message event to webhook url
func forwardMessageToWebhook(ctx context.Context, client *whatsmeow.Client, evt *events.Message) error {
webhookEvent, err := createWebhookEvent(ctx, client, evt)
func forwardMessageToWebhook(ctx context.Context, client *whatsmeow.Client, evt *events.Message, sessionID string) error {
webhookEvent, err := createWebhookEvent(ctx, client, evt, sessionID)
if err != nil {
return err
}
Expand All @@ -45,14 +46,18 @@ func forwardMessageToWebhook(ctx context.Context, client *whatsmeow.Client, evt
"device_id": webhookEvent.DeviceID,
"payload": webhookEvent.Payload,
}
if webhookEvent.SessionID != "" {
payload["session_id"] = webhookEvent.SessionID
}

return forwardPayloadToConfiguredWebhooks(ctx, payload, webhookEvent.Event)
}

func createWebhookEvent(ctx context.Context, client *whatsmeow.Client, evt *events.Message) (*WebhookEvent, error) {
func createWebhookEvent(ctx context.Context, client *whatsmeow.Client, evt *events.Message, sessionID string) (*WebhookEvent, error) {
webhookEvent := &WebhookEvent{
Event: EventTypeMessage,
Payload: make(map[string]any),
Event: EventTypeMessage,
SessionID: sessionID,
Payload: make(map[string]any),
}

// Set device_id
Expand Down
12 changes: 6 additions & 6 deletions src/infrastructure/whatsapp/event_message_handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
"go.mau.fi/whatsmeow/types/events"
)

func handleMessage(ctx context.Context, evt *events.Message, chatStorageRepo domainChatStorage.IChatStorageRepository, client *whatsmeow.Client) {
func handleMessage(ctx context.Context, evt *events.Message, chatStorageRepo domainChatStorage.IChatStorageRepository, sessionID string, client *whatsmeow.Client) {
// Log message metadata
metaParts := buildMessageMetaParts(evt)
log.Infof("Received message %s from %s (%s): %+v",
Expand All @@ -40,7 +40,7 @@ func handleMessage(ctx context.Context, evt *events.Message, chatStorageRepo dom
handleAutoReply(ctx, evt, chatStorageRepo, client)

// Forward to webhook if configured
handleWebhookForward(ctx, evt, client)
handleWebhookForward(ctx, evt, sessionID, client)
}

func buildMessageMetaParts(evt *events.Message) []string {
Expand Down Expand Up @@ -99,7 +99,7 @@ func handleAutoMarkRead(ctx context.Context, evt *events.Message, client *whatsm
}
}

func handleWebhookForward(ctx context.Context, evt *events.Message, client *whatsmeow.Client) {
func handleWebhookForward(ctx context.Context, evt *events.Message, sessionID string, client *whatsmeow.Client) {
// Skip webhook for protocol messages that are internal sync messages
if protocolMessage := evt.Message.GetProtocolMessage(); protocolMessage != nil {
protocolType := protocolMessage.GetType().String()
Expand All @@ -116,12 +116,12 @@ func handleWebhookForward(ctx context.Context, evt *events.Message, client *what

if (len(config.WhatsappWebhook) > 0 || config.ChatwootEnabled) &&
!strings.Contains(evt.Info.SourceString(), "broadcast") {
go func(e *events.Message, c *whatsmeow.Client) {
go func(e *events.Message, c *whatsmeow.Client, sid string) {
webhookCtx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := forwardMessageToWebhook(webhookCtx, c, e); err != nil {
if err := forwardMessageToWebhook(webhookCtx, c, e, sid); err != nil {
logrus.Error("Failed forward to webhook: ", err)
}
}(evt, client)
}(evt, client, sessionID)
Comment thread
occult marked this conversation as resolved.
}
}
Loading