Skip to content

Commit 9a677bc

Browse files
Merge branch 'main' into feat/chatwoot-multi-device
Keep main's ad_jid as migration 35 and renumber Chatwoot per-device config migrations to 36-42 so upgrades from main apply both schemas in order. Co-authored-by: Cursor <cursoragent@cursor.com>
2 parents 2056f11 + 0c81879 commit 9a677bc

26 files changed

Lines changed: 1543 additions & 240 deletions

docs/webhook-payload.md

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,45 @@ WHATSAPP_WEBHOOK_EVENTS=group.participants,group.joined,newsletter.joined,newsle
7979
- If configured, only the specified events are forwarded to webhooks
8080
- Event names are case-insensitive
8181

82+
### Ignoring chats/JIDs
83+
84+
In addition to filtering by **event type** above, you can skip events by **conversation or sender JID**
85+
for example, to mute all group traffic from the webhook. This is independent of `WHATSAPP_WEBHOOK_EVENTS`
86+
(the two filters compose: an event is forwarded only if its type is allowed **and** its JID is not ignored).
87+
88+
**Environment Variable:**
89+
90+
```bash
91+
# Drop all group messages/receipts (any chat_id ending in @g.us)
92+
WHATSAPP_WEBHOOK_IGNORE_JIDS=@g.us
93+
94+
# Drop all groups plus one specific 1:1 chat
95+
WHATSAPP_WEBHOOK_IGNORE_JIDS=@g.us,628123456789@s.whatsapp.net
96+
```
97+
98+
**CLI Flag:**
99+
100+
```bash
101+
./whatsapp rest --webhook="https://yourapp.com/webhook" --webhook-ignore-jids="@g.us"
102+
```
103+
104+
**Behavior:**
105+
106+
- Matches the event's `chat_id`, `from`, `chat_lid` or `from_lid` against the list (so an `@lid`
107+
pattern matches LID-migrated events, whose `@lid` JID lives in the `*_lid` fields).
108+
- An `@`-prefixed entry is an address-space **wildcard** (`@g.us`, `@s.whatsapp.net`, `@lid`); any other
109+
entry is an **exact** JID match.
110+
- Empty/unset (default) forwards everything.
111+
- Independent from the Chatwoot integration, which keeps its own `CHATWOOT_IGNORE_JIDS`.
112+
- `@g.us` is the recommended way to mute groups (it matches the group `chat_id`). The
113+
`@s.whatsapp.net` wildcard matches the **sender** too, so it also suppresses group messages (whose
114+
`from` is the participant's `@s.whatsapp.net` JID) — use exact JIDs if you only want to mute specific
115+
1:1 chats.
116+
- Note: a few events carry the group JID elsewhere or omit `chat_id``call.offer` puts the group in
117+
`group_jid` (not `chat_id`), `message.deleted` only includes `chat_id` when the original message is
118+
found locally, and `group.participants`/`group.joined` have no `from`. The `@g.us` wildcard still
119+
covers ordinary group messages and receipts (which is the common case for muting groups).
120+
82121
## Security
83122

84123
### HMAC Signature Verification

readme.md

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,13 @@ Download:
155155
| `call.offer` | Incoming call received |
156156

157157
If not configured (empty), all events will be forwarded.
158+
- **Webhook JID Filtering**
159+
160+
You can skip events for specific chats or senders (e.g. mute all groups) before they are forwarded:
161+
- `--webhook-ignore-jids="@g.us,628123456789@s.whatsapp.net"` (comma-separated list)
162+
- Or environment variable: `WHATSAPP_WEBHOOK_IGNORE_JIDS=@g.us`
163+
- Supports the `@g.us` / `@s.whatsapp.net` / `@lid` wildcards (match a whole address space) and exact JIDs.
164+
- This filters by conversation/sender and is independent of `--webhook-events` (which filters by event type). The Chatwoot integration keeps its own `CHATWOOT_IGNORE_JIDS`.
158165
- **Webhook TLS Configuration**
159166

160167
If you encounter TLS certificate verification errors when using webhooks (e.g., with Cloudflare tunnels or self-signed
@@ -222,8 +229,10 @@ To use environment variables:
222229
| `WHATSAPP_WEBHOOK_SECRET` | Webhook secret for validation | `secret` | `WHATSAPP_WEBHOOK_SECRET=super-secret-key` |
223230
| `WHATSAPP_WEBHOOK_INSECURE_SKIP_VERIFY` | Skip TLS verification for webhooks (insecure) | `false` | `WHATSAPP_WEBHOOK_INSECURE_SKIP_VERIFY=true` |
224231
| `WHATSAPP_WEBHOOK_EVENTS` | Whitelist of events to forward (comma-separated, empty = all) | - | `WHATSAPP_WEBHOOK_EVENTS=message,message.ack` |
232+
| `WHATSAPP_WEBHOOK_IGNORE_JIDS` | JIDs/wildcards to skip when forwarding (comma-separated) | - | `WHATSAPP_WEBHOOK_IGNORE_JIDS=@g.us` |
225233
| `WHATSAPP_ACCOUNT_VALIDATION` | Enable account validation | `true` | `WHATSAPP_ACCOUNT_VALIDATION=false` |
226234
| `WHATSAPP_PRESENCE_ON_CONNECT` | Presence on connect: `available`, `unavailable`, or `none` | `unavailable` | `WHATSAPP_PRESENCE_ON_CONNECT=unavailable` |
235+
| `WHATSAPP_PROXY` | Outbound proxy for the WhatsApp WebSocket (socks5/http/https) | - | `WHATSAPP_PROXY=socks5://user:pass@host:1080` |
227236
| `WHATSAPP_PRESENCE_PULSE_ENABLED` | Enable daily available/unavailable presence pulse | `true` | `WHATSAPP_PRESENCE_PULSE_ENABLED=false` |
228237
| `WHATSAPP_PRESENCE_PULSE_INTERVAL` | Interval between presence pulses | `24h` | `WHATSAPP_PRESENCE_PULSE_INTERVAL=24h` |
229238
| `WHATSAPP_PRESENCE_PULSE_DURATION` | Duration to stay available during each pulse | `5m` | `WHATSAPP_PRESENCE_PULSE_DURATION=5m` |
@@ -575,8 +584,8 @@ You can fork or edit this source code !
575584
| ✅ | Add Device | POST | /devices |
576585
| ✅ | Get Device Info | GET | /devices/:device_id |
577586
| ✅ | Remove Device | DELETE | /devices/:device_id |
578-
| | Login Device (QR, reserved) | GET | /devices/:device_id/login |
579-
| | Login Device (Code, reserved) | POST | /devices/:device_id/login/code |
587+
| | Login Device (QR) | GET | /devices/:device_id/login |
588+
| | Login Device (Code) | POST | /devices/:device_id/login/code |
580589
| ✅ | Logout Device | POST | /devices/:device_id/logout |
581590
| ✅ | Reconnect Device | POST | /devices/:device_id/reconnect |
582591
| ✅ | Get Device Status | GET | /devices/:device_id/status |
@@ -663,7 +672,6 @@ You can fork or edit this source code !
663672

664673
- `*User My Groups`: Returns a maximum of 500 groups due to WhatsApp protocol limitation. This is enforced by WhatsApp servers, not this API. See [whatsmeow source](https://github.com/tulir/whatsmeow/blob/main/group.go) for details.
665674
- `/health` is public and always registered at the root path, even when `APP_BASE_PATH` is set.
666-
- Device-specific login routes are reserved by the router but not implemented by the current usecase. Use `/app/login` or `/app/login-with-code` with `X-Device-Id` or `device_id` to pair a specific slot.
667675
- Chatwoot routes are registered only when `CHATWOOT_ENABLED=true`.
668676

669677
## User Interface

src/.env.example

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,13 +23,22 @@ WHATSAPP_WEBHOOK=https://webhook.site/07b69616-5943-4c7f-a8be-db4819df699e,https
2323
WHATSAPP_WEBHOOK_SECRET=super-secret-key
2424
WHATSAPP_WEBHOOK_INSECURE_SKIP_VERIFY=false
2525
WHATSAPP_WEBHOOK_EVENTS=message,message.reaction,message.revoked,message.edited,message.ack,message.deleted,group.participants
26+
# Comma-separated WhatsApp JIDs (or "@g.us"/"@s.whatsapp.net"/"@lid" wildcards) to skip when forwarding to the webhook.
27+
# Example: "@g.us" drops all group traffic; add exact JIDs to mute specific chats/senders.
28+
WHATSAPP_WEBHOOK_IGNORE_JIDS=
2629
WHATSAPP_WEBHOOK_INCLUDE_OUTGOING=false
2730
WHATSAPP_ACCOUNT_VALIDATION=true
2831
WHATSAPP_PRESENCE_ON_CONNECT=unavailable
2932
WHATSAPP_PRESENCE_PULSE_ENABLED=true
3033
WHATSAPP_PRESENCE_PULSE_INTERVAL=24h
3134
WHATSAPP_PRESENCE_PULSE_DURATION=5m
3235
WHATSAPP_CHAT_STORAGE=true
36+
# Outbound proxy applied to whatsmeow's WebSocket dialer via SetProxyAddress.
37+
# Standard HTTP_PROXY env does not apply to the underlying ws connection.
38+
# Useful for self-hosted deploys behind DPI / network egress restrictions.
39+
# Supports SOCKS5/HTTP/HTTPS schemes. Empty = direct connection.
40+
# Example: WHATSAPP_PROXY=socks5://user:pass@proxy.example.com:1080
41+
WHATSAPP_PROXY=
3342

3443
# Chatwoot Integration
3544
CHATWOOT_ENABLED=false

src/cmd/root.go

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,16 @@ func initEnvConfig() {
144144
events := strings.Split(envWebhookEvents, ",")
145145
config.WhatsappWebhookEvents = events
146146
}
147+
if envWebhookIgnoreJids := viper.GetString("whatsapp_webhook_ignore_jids"); envWebhookIgnoreJids != "" {
148+
parts := strings.Split(envWebhookIgnoreJids, ",")
149+
jids := make([]string, 0, len(parts))
150+
for _, p := range parts {
151+
if trimmed := strings.TrimSpace(p); trimmed != "" {
152+
jids = append(jids, trimmed)
153+
}
154+
}
155+
config.WhatsappWebhookIgnoreJids = jids
156+
}
147157
if viper.IsSet("whatsapp_account_validation") {
148158
config.WhatsappAccountValidation = viper.GetBool("whatsapp_account_validation")
149159
}
@@ -153,6 +163,12 @@ func initEnvConfig() {
153163
if envPresenceOnConnect := viper.GetString("whatsapp_presence_on_connect"); envPresenceOnConnect != "" {
154164
config.WhatsappPresenceOnConnect = envPresenceOnConnect
155165
}
166+
// Outbound proxy for whatsmeow WebSocket. Standard HTTP_PROXY env does not
167+
// apply to the underlying ws dialer; this binding plumbs the address into
168+
// (*whatsmeow.Client).SetProxyAddress before Connect. See issue #581.
169+
if envProxy := viper.GetString("whatsapp_proxy"); envProxy != "" {
170+
config.WhatsappProxy = envProxy
171+
}
156172
if viper.IsSet("whatsapp_presence_pulse_enabled") {
157173
config.WhatsappPresencePulseEnabled = viper.GetBool("whatsapp_presence_pulse_enabled")
158174
}
@@ -354,6 +370,12 @@ func initFlags() {
354370
config.WhatsappWebhookEvents,
355371
`whitelist of events to forward to webhook (empty = all events) --webhook-events <string> | example: --webhook-events="message,message.ack,group.participants"`,
356372
)
373+
rootCmd.PersistentFlags().StringSliceVarP(
374+
&config.WhatsappWebhookIgnoreJids,
375+
"webhook-ignore-jids", "",
376+
config.WhatsappWebhookIgnoreJids,
377+
`comma-separated WhatsApp JIDs (or "@g.us"/"@s.whatsapp.net"/"@lid" wildcards) to skip when forwarding to webhooks --webhook-ignore-jids <list> | example: --webhook-ignore-jids="@g.us,628123456789@s.whatsapp.net"`,
378+
)
357379
rootCmd.PersistentFlags().BoolVarP(
358380
&config.WhatsappAccountValidation,
359381
"account-validation", "",
@@ -372,6 +394,12 @@ func initFlags() {
372394
config.WhatsappPresenceOnConnect,
373395
`presence to send on connect: "available", "unavailable", or "none" --presence-on-connect <string> | example: --presence-on-connect="unavailable"`,
374396
)
397+
rootCmd.PersistentFlags().StringVarP(
398+
&config.WhatsappProxy,
399+
"whatsapp-proxy", "",
400+
config.WhatsappProxy,
401+
`outbound proxy for the WhatsApp WebSocket dialer --whatsapp-proxy <string> | example: --whatsapp-proxy="socks5://user:pass@host:1080"`,
402+
)
375403
rootCmd.PersistentFlags().BoolVarP(
376404
&config.WhatsappPresencePulseEnabled,
377405
"presence-pulse-enabled", "",
@@ -591,7 +619,7 @@ func initApp() {
591619
messageUsecase = usecase.NewMessageService(chatStorageRepo)
592620
groupUsecase = usecase.NewGroupService()
593621
newsletterUsecase = usecase.NewNewsletterService()
594-
deviceUsecase = usecase.NewDeviceService(dm)
622+
deviceUsecase = usecase.NewDeviceService(dm, appUsecase)
595623
}
596624

597625
// Execute adds all child commands to the root command and sets flags appropriately.

src/config/settings.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ var (
3535
WhatsappWebhookSecret = "secret"
3636
WhatsappWebhookInsecureSkipVerify = false // Skip TLS certificate verification for webhooks (insecure)
3737
WhatsappWebhookEvents []string // Whitelist of events to forward to webhook (empty = all events)
38+
WhatsappWebhookIgnoreJids []string // JIDs (or "@g.us"/"@s.whatsapp.net"/"@lid" wildcards) to skip when forwarding to webhooks
3839
WhatsappAutoRejectCall = false // Auto-reject incoming calls
3940
WhatsappLogLevel = "ERROR"
4041
WhatsappSettingMaxImageSize int64 = 20000000 // 20MB
@@ -51,6 +52,14 @@ var (
5152
WhatsappPresencePulseInterval = 24 * time.Hour
5253
WhatsappPresencePulseDuration = 5 * time.Minute
5354

55+
// WhatsappProxy is forwarded to whatsmeow's *Client.SetProxyAddress before
56+
// Connect. Accepts SOCKS5/HTTP/HTTPS schemes, e.g.
57+
// "socks5://user:pass@host:1080" or "http://host:8080". Empty = direct
58+
// (no proxy). Useful for self-hosted deployments behind DPI / network
59+
// egress restrictions where standard HTTP_PROXY env vars do not apply
60+
// to the WhatsApp WebSocket dialer.
61+
WhatsappProxy = ""
62+
5463
ChatStorageURI = "file:storages/chatstorage.db"
5564
ChatStorageEnableForeignKeys = true
5665
ChatStorageEnableWAL = true

src/domains/chatstorage/chatstorage.go

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -124,24 +124,29 @@ type MediaInfo struct {
124124
}
125125

126126
// DeviceRecord tracks a registered device for persistence purposes.
127+
// JID holds the bare-number (NonAD) form used for chat storage partitioning; ADJID
128+
// holds the full companion identity (number:NN@s.whatsapp.net) that pins the slot to
129+
// one specific whatsmeow session. ADJID is empty until the first connect after
130+
// pairing (the :NN suffix is only known once the socket authenticates).
127131
type DeviceRecord struct {
128-
DeviceID string `db:"device_id"`
129-
DisplayName string `db:"display_name"`
130-
JID string `db:"jid"`
131-
WebhookURL *string `db:"webhook_url"`
132-
WebhookSecret string `db:"webhook_secret"`
133-
WebhookEvents string `db:"webhook_events"`
132+
DeviceID string `db:"device_id"`
133+
DisplayName string `db:"display_name"`
134+
JID string `db:"jid"`
135+
ADJID string `db:"ad_jid"`
136+
WebhookURL *string `db:"webhook_url"`
137+
WebhookSecret string `db:"webhook_secret"`
138+
WebhookEvents string `db:"webhook_events"`
134139
WebhookInsecureSkipVerify bool `db:"webhook_insecure_skip_verify"`
135-
CreatedAt time.Time `db:"created_at"`
136-
UpdatedAt time.Time `db:"updated_at"`
140+
CreatedAt time.Time `db:"created_at"`
141+
UpdatedAt time.Time `db:"updated_at"`
137142
}
138143

139144
// DeviceWebhookConfig holds the complete webhook configuration for a device.
140145
type DeviceWebhookConfig struct {
141-
WebhookURL *string `json:"webhook_url,omitempty"`
142-
WebhookSecret string `json:"webhook_secret,omitempty"`
143-
WebhookEvents string `json:"webhook_events,omitempty"`
144-
WebhookInsecureSkipVerify bool `json:"webhook_insecure_skip_verify,omitempty"`
146+
WebhookURL *string `json:"webhook_url,omitempty"`
147+
WebhookSecret string `json:"webhook_secret,omitempty"`
148+
WebhookEvents string `json:"webhook_events,omitempty"`
149+
WebhookInsecureSkipVerify bool `json:"webhook_insecure_skip_verify,omitempty"`
145150
}
146151

147152
// MessageFilter represents query filters for messages

src/domains/chatstorage/interfaces.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,9 @@ type IChatStorageRepository interface {
8585
SaveDeviceRecord(record *DeviceRecord) error
8686
ListDeviceRecords() ([]*DeviceRecord, error)
8787
GetDeviceRecord(deviceID string) (*DeviceRecord, error)
88-
// GetDeviceRecordByJID fetches a device record by its JID.
88+
// GetDeviceRecordByJID fetches a device record by its JID. A full AD JID resolves
89+
// the exact slot; a bare-number JID resolves only while unambiguous — when several
90+
// slots share the number it returns nil rather than an arbitrary sibling's record.
8991
GetDeviceRecordByJID(jid string) (*DeviceRecord, error)
9092
DeleteDeviceRecord(deviceID string) error
9193
// SetDeviceWebhookURL sets the webhook URL for a device.

src/domains/device/interfaces.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package device
33
import (
44
"context"
55

6+
"github.com/aldinokemal/go-whatsapp-web-multidevice/domains/app"
67
"github.com/aldinokemal/go-whatsapp-web-multidevice/domains/chatstorage"
78
)
89

@@ -14,7 +15,7 @@ type IDeviceUsecase interface {
1415
// webhook configuration (url, secret, events, insecure_skip_verify) at creation time.
1516
AddDevice(ctx context.Context, deviceID string, webhook *chatstorage.DeviceWebhookConfig) (*Device, error)
1617
RemoveDevice(ctx context.Context, deviceID string) error
17-
LoginDevice(ctx context.Context, deviceID string) error
18+
LoginDevice(ctx context.Context, deviceID string) (app.LoginResponse, error)
1819
LoginDeviceWithCode(ctx context.Context, deviceID string, phone string) (string, error)
1920
LogoutDevice(ctx context.Context, deviceID string) error
2021
ReconnectDevice(ctx context.Context, deviceID string) error

src/go.mod

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ require (
2222
github.com/stretchr/testify v1.11.1
2323
github.com/valyala/fasthttp v1.71.0
2424
go.mau.fi/libsignal v0.2.2
25-
go.mau.fi/whatsmeow v0.0.0-20260630180629-b572e5bcb92b
25+
go.mau.fi/whatsmeow v0.0.0-20260709092057-73fe7355f59f
2626
golang.org/x/image v0.41.0
2727
google.golang.org/protobuf v1.36.11
2828
modernc.org/sqlite v1.50.1
@@ -68,12 +68,12 @@ require (
6868
github.com/yosida95/uritemplate/v3 v3.0.2 // indirect
6969
go.mau.fi/util v0.9.10 // indirect
7070
go.yaml.in/yaml/v3 v3.0.4 // indirect
71-
golang.org/x/crypto v0.53.0 // indirect
71+
golang.org/x/crypto v0.54.0 // indirect
7272
golang.org/x/exp v0.0.0-20260611194520-c48552f49976 // indirect
73-
golang.org/x/net v0.56.0 // indirect
74-
golang.org/x/sync v0.21.0 // indirect
75-
golang.org/x/sys v0.46.0 // indirect
76-
golang.org/x/text v0.38.0 // indirect
73+
golang.org/x/net v0.57.0 // indirect
74+
golang.org/x/sync v0.22.0 // indirect
75+
golang.org/x/sys v0.47.0 // indirect
76+
golang.org/x/text v0.40.0 // indirect
7777
gopkg.in/yaml.v3 v3.0.1 // indirect
7878
modernc.org/libc v1.72.3 // indirect
7979
modernc.org/mathutil v1.7.1 // indirect

0 commit comments

Comments
 (0)