whatsapp_notifier 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 130b5676184b753e6329f98ead313255db4a16be9393597f4b34f38741ae9c63
4
- data.tar.gz: 7c7fe4a7eb0464676d4257443fa3003e80035e938e41d690aee7282787fd962a
3
+ metadata.gz: 5516496d221334794f99898b801a9277c7e056971198cc7cb6577df6499c0d72
4
+ data.tar.gz: 2e06a216928933ba02bef20de6be794d089205effcd76ae5784d992776263fa8
5
5
  SHA512:
6
- metadata.gz: 18e39291eabaa4a61e33a7a9f62a840b233320724147dc24614f740dafaa14151363a2e606943027098b42bed3dbea80db81459d1c73a38d1ad6989d0a1d2104
7
- data.tar.gz: 35170c14888aa976a0eb211757e617a2237ab8e442ee75b9e319391d1737bbb75ce81d3a69802b71b9f712ab8d8731e4fd1d243123017cac05c5c686cac9830c
6
+ metadata.gz: 256eecd211ade964aca1453635dc0ca3a55f9d422473bf9fe26d52f793f429e4d78b6071b9906da749c1c2fd983a4ea3047f58b545a758421d9c819c55b03b4a
7
+ data.tar.gz: a0f2dca29b80d6d39b9d65ab7b924e31f5e4c39fcfc2ff8f0546514671ae4089e0131b30564338fc0f8dccc3d5ee1af691cf2903d272a7b565fea8fa04894882
data/README.md CHANGED
@@ -1,88 +1,246 @@
1
- # WhatsAppNotifier
1
+ # whatsapp_notifier
2
+
3
+ [![Gem Version](https://img.shields.io/gem/v/whatsapp_notifier.svg)](https://rubygems.org/gems/whatsapp_notifier)
4
+ [![CI](https://github.com/kshtzkr/whatsapp_notifier/actions/workflows/ci.yml/badge.svg)](https://github.com/kshtzkr/whatsapp_notifier/actions/workflows/ci.yml)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE.txt)
6
+ [![Ruby](https://img.shields.io/badge/ruby-%3E%3D%202.6-CC342D.svg)](whatsapp_notifier.gemspec)
7
+
8
+ Add WhatsApp messaging to a Rails app with one install generator and one service
9
+ command.
10
+
11
+ The gem gives you a small Ruby API (`WhatsAppNotifier.deliver`, mailer-style
12
+ notification classes, and paced bulk delivery) plus a bundled Bun service that
13
+ holds the WhatsApp Web session. No Meta developer account, app review, or webhook
14
+ configuration is required, because messages go through a linked WhatsApp Web
15
+ session rather than the official Business API.
16
+
17
+ That trade-off matters: this drives an unofficial WhatsApp Web automation
18
+ session, so use it for consent-based, low-volume messaging and read
19
+ [docs/bulk_messaging_policy.md](docs/bulk_messaging_policy.md) before you send at
20
+ scale. See [How it works](#how-it-works) for the service boundary.
21
+
22
+ ## Table of contents
23
+
24
+ - [Requirements](#requirements)
25
+ - [How it works](#how-it-works)
26
+ - [Installation](#installation)
27
+ - [Quick start](#quick-start)
28
+ - [Connecting a number](#connecting-a-number)
29
+ - [Sending a message](#sending-a-message)
30
+ - [Notification classes](#notification-classes)
31
+ - [Bulk delivery](#bulk-delivery)
32
+ - [Receiving messages and media](#receiving-messages-and-media)
33
+ - [Command line](#command-line)
34
+ - [Mounted engine routes](#mounted-engine-routes)
35
+ - [Configuration](#configuration)
36
+ - [Logging out](#logging-out)
37
+ - [Testing](#testing)
38
+ - [Development](#development)
39
+ - [Contributing](#contributing)
40
+ - [Security](#security)
41
+ - [Versioning](#versioning)
42
+ - [License](#license)
43
+
44
+ ## Requirements
45
+
46
+ - Ruby 2.6 or newer (the gemspec sets `required_ruby_version >= 2.6.0`; CI runs
47
+ on 3.2, 3.3, and 3.4).
48
+ - Runtime gem dependencies: `logger >= 1.5` and `thor >= 1.0`.
49
+ - Rails for the mountable engine and the install generators. The engine
50
+ registers a Zeitwerk inflection, so it needs Rails 6 or newer. The plain Ruby
51
+ API (`WhatsAppNotifier.deliver`, notification classes, bulk delivery) works
52
+ without Rails.
53
+ - [Bun](https://bun.sh) to run the bundled WhatsApp Web service.
54
+ - Chromium or Chrome reachable by the service. Puppeteer drives it; set
55
+ `PUPPETEER_EXECUTABLE_PATH` if the binary is not on a standard path.
56
+
57
+ ## How it works
58
+
59
+ Two processes cooperate:
60
+
61
+ 1. Your Ruby or Rails app, using this gem. `WhatsAppNotifier::WebAdapter` speaks
62
+ JSON over HTTP to the service (default `http://127.0.0.1:3001`).
63
+ 2. The bundled service under `lib/whatsapp_notifier/services/web_automation`. It
64
+ runs on Bun (Hono + `whatsapp-web.js`) and drives a headless Chromium against
65
+ `web.whatsapp.com`. It keeps one WhatsApp Web client per `user_id` and
66
+ persists each session on disk so restarts reconnect without a new QR.
67
+
68
+ Nothing here touches the official WhatsApp Business API. The service pairs like
69
+ WhatsApp Web on a laptop, which is why setup needs no Meta account, and why the
70
+ bulk-delivery guardrails exist. Point the gem at a remote service with
71
+ `WHATSAPP_NOTIFIER_SERVICE_URL` (or `WHATSAPP_SERVICE_URL`); `https://` URLs are
72
+ honored natively.
2
73
 
3
- `whatsapp_notifier` is production-ready WhatsApp messaging for Rails.
4
- Set it up in minutes with one install generator and one service command.
74
+ ## Installation
5
75
 
6
- No official WhatsApp API setup, app review, or Meta webhook configuration is required.
76
+ Add the gem:
7
77
 
8
- ## What You Get
78
+ ```ruby
79
+ # Gemfile
80
+ gem "whatsapp_notifier"
81
+ ```
9
82
 
10
- - Bun-powered lightweight WhatsApp Web service (embedded in the gem)
11
- - QR scanning and connection status APIs
12
- - Single message and bulk message delivery APIs
13
- - Mailer-like notification classes with `deliver_now` / `deliver_later`
14
- - Multi-user session support via `metadata[:user_id]`
83
+ ```bash
84
+ bundle install
85
+ ```
15
86
 
16
- ## Quick Start (60 seconds)
87
+ Then run the install generator:
17
88
 
18
89
  ```bash
19
- bundle add whatsapp_notifier
20
90
  bin/rails g whatsapp_notifier:install
21
- bin/dev
22
91
  ```
23
92
 
24
- Then:
93
+ The generator:
94
+
95
+ - writes `config/initializers/whatsapp_notifier.rb`,
96
+ - mounts the engine with `mount WhatsAppNotifier::Engine, at: "/whatsapp"`,
97
+ - adds `whatsapp: bundle exec whatsapp_notifier service` to `Procfile.dev`,
98
+ - adds session and service paths to `.gitignore`,
99
+ - runs `whatsapp_notifier doctor` to check your setup.
25
100
 
26
- - open `/dashboard/whatsapp/qr`
27
- - scan QR
28
- - send a test message
101
+ The gem ships no migrations, so there is no `db:migrate` step.
29
102
 
30
- If setup fails, run:
103
+ Start the service alongside your app. With the `Procfile.dev` entry above,
104
+ `bin/dev` runs it; on its own:
31
105
 
32
106
  ```bash
33
- bundle exec whatsapp_notifier doctor
107
+ bundle exec whatsapp_notifier service
34
108
  ```
35
109
 
36
- ## Installation
110
+ The first run installs the service's dependencies with `bun install`, then
111
+ launches it on port 3001.
112
+
113
+ ## Quick start
114
+
115
+ ```bash
116
+ bundle add whatsapp_notifier
117
+ bin/rails g whatsapp_notifier:install
118
+ bin/dev
119
+ ```
120
+
121
+ With the service running, connect a number (see below), then send:
37
122
 
38
123
  ```ruby
39
- gem "whatsapp_notifier"
124
+ result = WhatsAppNotifier.deliver(
125
+ to: "+919999999999",
126
+ body: "Booking confirmed",
127
+ metadata: { user_id: current_user.id }
128
+ )
129
+
130
+ result.success? # => true
40
131
  ```
41
132
 
133
+ If setup fails, run the diagnostics:
134
+
42
135
  ```bash
43
- bundle install
44
- bin/rails g whatsapp_notifier:install
136
+ bundle exec whatsapp_notifier doctor
45
137
  ```
46
138
 
47
- ## Scan QR and check status
139
+ ## Connecting a number
140
+
141
+ Each linked number is a session keyed by `metadata[:user_id]`. Omit the key to
142
+ use a single shared `default` session; pass `current_user.id` (or any stable id)
143
+ for per-user sessions.
144
+
145
+ Fetch the current QR and render it. The service returns a `data:` PNG URL, or
146
+ `nil` while the client is still booting:
48
147
 
49
148
  ```ruby
50
- # Use current_user.id for multi-user apps; omit metadata for a default shared session.
51
149
  qr_data_url = WhatsAppNotifier.scan_qr(metadata: { user_id: current_user.id })
150
+ # => "data:image/png;base64,..." or nil
151
+ ```
152
+
153
+ Show that image, scan it from the phone (WhatsApp, Linked devices, Link a
154
+ device), then poll the connection status until it reports authenticated:
52
155
 
156
+ ```ruby
53
157
  status = WhatsAppNotifier.connection_status(metadata: { user_id: current_user.id })
54
- # => { state: "...", authenticated: true/false, has_qr: true/false }
158
+ # => { state: "AUTHENTICATED", authenticated: true, has_qr: false }
55
159
  ```
56
160
 
57
- ## Log out (disconnect + clear session)
161
+ The QR rotates every ~20 seconds, so re-fetch it while `has_qr` is true. Once a
162
+ session authenticates it persists on the service, so later sends reconnect
163
+ without a new scan.
58
164
 
59
- Explicitly disconnects the user and wipes their saved WhatsApp session from the
60
- service including any downloaded inbound media and queued replies, which
61
- belong to the old pairing so the next connect starts fresh with a new QR.
62
- Call this from a user-initiated "Log out WhatsApp" action — NOT from your
63
- app's sign-out, which should leave the WhatsApp session intact for the next
64
- login.
165
+ Before sending, `session_ready?` answers the only question a sender actually
166
+ has. It is `connection_status` reduced to a boolean, with transport failures
167
+ treated as not-readyan unreachable status endpoint means the service is down,
168
+ so the send could not have succeeded either:
65
169
 
66
170
  ```ruby
67
- WhatsAppNotifier.logout(metadata: { user_id: current_user.id })
68
- # => { success: true }
171
+ WhatsAppNotifier.session_ready?(user_id: current_user.id)
172
+ # => true / false, never raises on a network error
69
173
  ```
70
174
 
71
- The mounted engine also exposes `DELETE /whatsapp/logout` for the same effect.
175
+ A `ConfigurationError` still raises: that is a mistake in your setup, and
176
+ answering `false` would quietly park every send behind a "session down" retry.
177
+
178
+ The mounted engine exposes the same two steps as JSON endpoints
179
+ (`GET /whatsapp/qr` and `GET /whatsapp/status`) if you prefer to build the
180
+ pairing screen in your front end.
72
181
 
73
- ## Send a message
182
+ ## Sending a message
74
183
 
75
184
  ```ruby
76
185
  result = WhatsAppNotifier.deliver(
77
186
  to: "+919999999999",
78
- body: "Booking confirmed",
187
+ body: "Payment received",
79
188
  metadata: { user_id: current_user.id }
80
189
  )
190
+ ```
81
191
 
82
- result.success?
192
+ `deliver` returns a `WhatsAppNotifier::Result`:
193
+
194
+ | Method | Meaning |
195
+ | --- | --- |
196
+ | `success?` / `failure?` | delivery outcome |
197
+ | `message_id` | WhatsApp message id from the service (a local fallback id on older services) |
198
+ | `error_code` | machine-readable failure class on failure, `nil` on success |
199
+ | `error_message` | human-readable failure reason |
200
+ | `wait_seconds` | provider-requested backoff, honored by bulk delivery |
201
+ | `metadata` | provider metadata hash |
202
+
203
+ ### Failure codes
204
+
205
+ `error_code` is a stable symbol you can branch on. The important distinction is
206
+ whether the message could already be on its way: retrying a send whose outcome
207
+ is unknown may message someone twice.
208
+
209
+ | Code | Meaning | Did the message go out? |
210
+ | --- | --- | --- |
211
+ | `:auth_required` | session logged out, or the service refused upfront | no |
212
+ | `:not_on_whatsapp` | the number has no WhatsApp account | no |
213
+ | `:invalid_phone` | the number is malformed | no |
214
+ | `:recipient_unresolved` | the service could not resolve the recipient id (wedged session) | no |
215
+ | `:service_unreachable` | the connection never opened | no |
216
+ | `:timeout` | the request went out, the answer never came | **unknown** |
217
+ | `:rate_limited` | the service throttled the send | **unknown** |
218
+ | `:delivery_exception` | unclassified | **unknown** |
219
+
220
+ `:delivery_exception` is the catch-all and the pre-0.9.0 value for every
221
+ failure, so code that already keys on it keeps working. `error_message` is
222
+ unchanged too — including the `"service request failed (401): ..."` wording —
223
+ so a host can migrate off text fingerprints at its own pace.
224
+
225
+ To attach a file, pass its URL as `metadata[:media_url]`:
226
+
227
+ ```ruby
228
+ WhatsAppNotifier.deliver(
229
+ to: "+919999999999",
230
+ body: "Here is your invoice",
231
+ metadata: { user_id: current_user.id, media_url: invoice_url }
232
+ )
83
233
  ```
84
234
 
85
- ## Notifications API
235
+ The engine also exposes `POST /whatsapp/send` with `to` (or `phone`), `message`,
236
+ and optional `media_url`.
237
+
238
+ ## Notification classes
239
+
240
+ Model messages as mailer-style classes. Define `#message` (and `#to`), or use a
241
+ class-level template.
242
+
243
+ Method style:
86
244
 
87
245
  ```ruby
88
246
  class LeadWhatsappNotification < WhatsAppNotifier::Notification
@@ -98,13 +256,35 @@ class LeadWhatsappNotification < WhatsAppNotifier::Notification
98
256
  { user_id: params[:user].id }
99
257
  end
100
258
  end
259
+
260
+ LeadWhatsappNotification.with(lead: @lead, user: current_user).deliver_later
101
261
  ```
102
262
 
263
+ Template style. Template variables come from the nested `params:` hash and fill
264
+ `{{placeholders}}`:
265
+
103
266
  ```ruby
104
- LeadWhatsappNotification.with(lead: @lead, user: current_user).deliver_later
267
+ class PaymentAlertNotification < WhatsAppNotifier::Notification
268
+ to "+919999999999"
269
+ provider :web_automation
270
+ template :payment_alert, "Hi {{name}}, payment {{amount}} received."
271
+
272
+ def metadata
273
+ { user_id: params[:user_id] }
274
+ end
275
+ end
276
+
277
+ PaymentAlertNotification.deliver_now(
278
+ params: { name: "Riya", amount: "INR 1500" },
279
+ user_id: 42
280
+ )
105
281
  ```
106
282
 
107
- ## Bulk Messaging
283
+ `deliver_later` runs through ActiveJob (queue `:default`) when it is available in
284
+ the host app. Without ActiveJob loaded it falls back to running inline, so the
285
+ call still delivers.
286
+
287
+ ## Bulk delivery
108
288
 
109
289
  ```ruby
110
290
  messages = [
@@ -113,78 +293,187 @@ messages = [
113
293
  ]
114
294
 
115
295
  summary = WhatsAppNotifier.deliver_bulk(messages)
116
- summary[:success]
296
+ # => { total: 2, success: 2, failed: 0, results: [#<Result>, #<Result>] }
117
297
  ```
118
298
 
119
- ## Generators
299
+ Each message accepts `to`, `body`, `metadata`, and an optional
300
+ `idempotency_key`. The dispatcher:
301
+
302
+ - paces sends with `bulk_base_delay_seconds` plus up to `bulk_jitter_seconds` of
303
+ random jitter,
304
+ - rejects a batch larger than `bulk_max_recipients`,
305
+ - retries a failed send up to `bulk_max_attempts` times, but only when its
306
+ `error_code` is in `bulk_retryable_error_codes`,
307
+ - sleeps for `wait_seconds` when a result asks for backoff,
308
+ - skips a repeated `idempotency_key` within the same run.
309
+
310
+ See [docs/bulk_messaging_policy.md](docs/bulk_messaging_policy.md) for the
311
+ guardrails and recommended limits.
312
+
313
+ ## Receiving messages and media
314
+
315
+ The service captures both sides of each 1:1 conversation and buffers inbound
316
+ messages per user. Drain them (at-least-once; dedupe on `message_id`):
317
+
318
+ ```ruby
319
+ WhatsAppNotifier.fetch_inbound(metadata: { user_id: current_user.id })
320
+ # => [{ from:, body:, message_id:, timestamp:, type:, ... }]
321
+ ```
120
322
 
121
- Install everything with one command:
323
+ Related helpers, each keyed by the same `metadata`:
324
+
325
+ - `WhatsAppNotifier.fetch_media(message_id:, metadata:)` returns
326
+ `{ body:, mime:, filename:, size: }`, or `nil` when the service has no copy.
327
+ - `WhatsAppNotifier.refetch_media(message_id:, chat_id:, metadata:)` re-downloads
328
+ one message's media on demand.
329
+ - `WhatsAppNotifier.delete_media(message_id:, metadata:)` drops the service's
330
+ copy after you have stored the bytes.
331
+ - `WhatsAppNotifier.list_chats(metadata:)` lists the paired number's 1:1 chats.
332
+ - `WhatsAppNotifier.fetch_history(chat_id:, limit: 50, metadata:)` replays one
333
+ chat's recent history.
334
+
335
+ The engine also exposes `GET /whatsapp/inbound` for host apps that would rather
336
+ pull through Rails. The `CHANGELOG.md` records when each capability landed and
337
+ the wire-compatibility rules between gem and service versions.
338
+
339
+ ## Command line
340
+
341
+ The `whatsapp_notifier` executable (Thor) has two commands.
122
342
 
123
343
  ```bash
124
- rails generate whatsapp_notifier:install
344
+ # Start the bundled WhatsApp Web service (Bun). --port overrides the default 3001.
345
+ bundle exec whatsapp_notifier service [--port 3001]
346
+
347
+ # Validate local setup and print exact fixes.
348
+ bundle exec whatsapp_notifier doctor
125
349
  ```
126
350
 
127
- If you want to eject Bun service files into your app:
351
+ `doctor` checks that Bun is installed, that a Chromium-compatible browser is
352
+ found (or `PUPPETEER_EXECUTABLE_PATH` is set), that the session directory is
353
+ writable, and that the service URL is well-formed. It exits non-zero and prints
354
+ a fix for each failed check.
355
+
356
+ `service` sets `PORT`, defaults `WHATSAPP_SESSION_DIR` to a writable path under
357
+ the app, autodetects Chromium, installs the service's dependencies on first run,
358
+ then execs `bun index.ts`.
359
+
360
+ ## Mounted engine routes
361
+
362
+ `whatsapp_notifier:install` mounts the engine at `/whatsapp`. Every action
363
+ returns JSON.
364
+
365
+ | Method | Path | Action | Purpose |
366
+ | --- | --- | --- | --- |
367
+ | GET | `/whatsapp/status` | `sessions#show` | connection state for the current user |
368
+ | GET | `/whatsapp/qr` | `sessions#qr` | latest QR (`nil` while initializing) |
369
+ | DELETE | `/whatsapp/logout` | `sessions#destroy` | disconnect and clear the session |
370
+ | POST | `/whatsapp/send` | `messages#create` | send one message |
371
+ | GET | `/whatsapp/inbound` | `messages#inbound` | drain pending inbound messages |
372
+
373
+ The engine resolves the current user through
374
+ `config.current_user_id_resolver` and can run an auth hook through
375
+ `config.authenticate_with` (see [Configuration](#configuration)). To eject the
376
+ Bun service source into your app instead of running it from the gem:
128
377
 
129
378
  ```bash
130
379
  rails generate whatsapp_notifier:install_service
131
380
  ```
132
381
 
133
- This copies the service to `whatsapp_service/` and updates `.gitignore`.
134
-
135
- ## Service health probe
136
-
137
- The Bun service exposes `GET /health` for load balancer / platform probes
138
- (Azure, Kubernetes, uptime checks). It reads only in-memory state — it never
139
- creates a WhatsApp client — so it is safe to poll every few seconds:
140
-
141
- ```json
142
- { "ok": true, "uptime_s": 4242, "sessions": 3, "ready": 2 }
143
- ```
144
-
145
- `sessions` is the number of clients held in memory; `ready` is how many of
146
- those have a fully hydrated WhatsApp Web store. Prometheus metrics live at
147
- `GET /metrics`.
148
-
149
- ## Upgrading to 0.7.0
150
-
151
- - **If your app monkey-patches `WhatsAppNotifier::WebAdapter#request`** (a
152
- common trick to bolt TLS onto the service URL): the shared JSON path now
153
- also carries `DELETE` (`delete_media`) and attaches `X-WA-Token` when
154
- `WHATSAPP_WEBHOOK_TOKEN` is set. A patch that only routes `POST`/`GET` or
155
- drops headers will break the new media calls. Prefer retiring the patch
156
- entirely the adapter now honors `https://` service URLs natively on both
157
- the JSON control plane and the binary media path (`use_ssl` follows the URL
158
- scheme), so TLS no longer needs a patch.
159
- - **Logout now wipes stored media**: `POST /logout` removes the user's
160
- downloaded inbound media along with the session dir and queued replies.
161
- Fetch (and ideally `delete_media`) anything you want to keep before logging
162
- a user out.
163
-
164
- ## Notes
165
-
166
- - This gem uses WhatsApp Web automation. Use responsibly and follow WhatsApp policies.
167
- - Keep Chromium available in your runtime (or set `PUPPETEER_EXECUTABLE_PATH`).
168
- - Session profiles persist under `WHATSAPP_SESSION_DIR`. Careful: the
169
- `whatsapp_notifier service` CLI launcher defaults it to
170
- `./tmp/whatsapp_notifier/.wwebjs_auth` **relative to the current working
171
- directory** (only the bare Bun service falls back to `/whatsapp_data`).
172
- Production must set `WHATSAPP_SESSION_DIR` explicitly to a durable mount
173
- (e.g. `/whatsapp_data`) so logins survive restarts and redeploys; the
174
- service clears stale Chromium `SingletonLock` files on each launch so an
175
- unclean exit can't wedge it.
176
- - Resilience knobs: `WHATSAPP_INIT_TIMEOUT_MS` (default 90000) recycles a client
177
- that never reaches QR/READY; `WHATSAPP_UNREADY_REAP_MS` (default 1800000)
178
- destroys non-ready clients idle that long (abandoned pairing screens; the
179
- cleanup sweep runs every 5 minutes, so reaping lands within ~5 minutes of
180
- the limit)
181
- sessions that authenticated at least once keep their on-disk dir for
182
- reconnect, while a pairing that never authenticated has its credential-less
183
- dir removed too, so it cannot pass the paired-session gate later; set
184
- `WWEBJS_WEB_VERSION` (e.g. `2.3000.1023204887`,
185
- optionally `WWEBJS_WEB_VERSION_CACHE_URL`) to pin the WhatsApp Web build so a
186
- live web.whatsapp.com change can't silently break the client.
382
+ This copies the service files to `whatsapp_service/` and updates `.gitignore`.
383
+
384
+ ## Configuration
385
+
386
+ Configure the gem in `config/initializers/whatsapp_notifier.rb`:
387
+
388
+ ```ruby
389
+ WhatsAppNotifier.configure do |config|
390
+ config.provider = :web_automation
391
+ config.web_automation_enabled = true
392
+ config.bulk_base_delay_seconds = 1.2
393
+ config.bulk_jitter_seconds = 0.4
394
+ config.bulk_max_recipients = 300
395
+ end
396
+ ```
397
+
398
+ The most common options and their defaults:
399
+
400
+ | Option | Default | Purpose |
401
+ | --- | --- | --- |
402
+ | `provider` | `:web_automation` | messaging backend; only `:web_automation` is supported |
403
+ | `web_session_path` | `"tmp/whatsapp_notifier/session.json"` | where the gem stores session pointers |
404
+ | `bulk_base_delay_seconds` | `1.0` | base pause between bulk sends |
405
+ | `bulk_jitter_seconds` | `0.3` | random jitter added to each bulk pause |
406
+ | `bulk_max_recipients` | `500` | largest batch `deliver_bulk` accepts |
407
+ | `bulk_max_attempts` | `3` | attempts per message in bulk retries |
408
+ | `authenticate_with` | `nil` | callable run as a `before_action` in the engine |
409
+ | `current_user_id_resolver` | resolves `current_user.id` | how the engine identifies the user |
410
+
411
+ The full reference (every configuration option and every service environment
412
+ variable, with types and defaults) is in
413
+ [docs/CONFIGURATION.md](docs/CONFIGURATION.md). For a longer Rails walk-through,
414
+ see [docs/rails_setup.md](docs/rails_setup.md).
415
+
416
+ ## Logging out
417
+
418
+ `logout` disconnects the user and wipes their saved WhatsApp session on the
419
+ service, including downloaded inbound media and queued replies, so the next
420
+ connect starts fresh with a new QR:
421
+
422
+ ```ruby
423
+ WhatsAppNotifier.logout(metadata: { user_id: current_user.id })
424
+ # => { success: true }
425
+ ```
426
+
427
+ Call this from a user-initiated "Log out WhatsApp" action, not from your app's
428
+ sign-out, which should leave the session intact for the next login. The engine
429
+ exposes `DELETE /whatsapp/logout` for the same effect.
430
+
431
+ ## Testing
432
+
433
+ Run the Ruby suite (SimpleCov enforces 100% line coverage):
434
+
435
+ ```bash
436
+ bundle exec rspec
437
+ ```
438
+
439
+ The bundled service has its own tests:
440
+
441
+ ```bash
442
+ cd lib/whatsapp_notifier/services/web_automation
443
+ bun install
444
+ bun test
445
+ ```
446
+
447
+ CI runs both jobs (`.github/workflows/ci.yml`): RSpec on Ruby 3.2–3.4 and the
448
+ Bun service tests.
449
+
450
+ ## Development
451
+
452
+ ```bash
453
+ git clone https://github.com/kshtzkr/whatsapp_notifier.git
454
+ cd whatsapp_notifier
455
+ bundle install
456
+ bundle exec rspec
457
+ ```
458
+
459
+ The gemspec requires MFA for RubyGems pushes (`rubygems_mfa_required`).
460
+
461
+ ## Contributing
462
+
463
+ Bug reports and pull requests are welcome. See
464
+ [CONTRIBUTING.md](CONTRIBUTING.md) for setup, the branch and PR flow, and the
465
+ commit style. By participating you agree to the
466
+ [Code of Conduct](CODE_OF_CONDUCT.md).
467
+
468
+ ## Security
469
+
470
+ Please report vulnerabilities privately. See [SECURITY.md](SECURITY.md) for how.
471
+
472
+ ## Versioning
473
+
474
+ This project follows [Semantic Versioning](https://semver.org). Notable changes
475
+ are recorded in [CHANGELOG.md](CHANGELOG.md).
187
476
 
188
477
  ## License
189
478
 
190
- MIT. See `LICENSE.txt`.
479
+ Released under the MIT License. See [LICENSE.txt](LICENSE.txt).