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 +4 -4
- data/README.md +391 -102
- data/docs/CONFIGURATION.md +89 -0
- data/lib/whatsapp_notifier/client.rb +4 -0
- data/lib/whatsapp_notifier/error_code.rb +137 -0
- data/lib/whatsapp_notifier/errors.rb +21 -0
- data/lib/whatsapp_notifier/providers/base.rb +18 -0
- data/lib/whatsapp_notifier/providers/web_automation.rb +14 -3
- data/lib/whatsapp_notifier/services/web_automation/history.test.ts +68 -1
- data/lib/whatsapp_notifier/services/web_automation/history.ts +34 -7
- data/lib/whatsapp_notifier/services/web_automation/inbound.test.ts +120 -6
- data/lib/whatsapp_notifier/services/web_automation/inbound.ts +115 -16
- data/lib/whatsapp_notifier/services/web_automation/index.ts +59 -0
- data/lib/whatsapp_notifier/services/web_automation/metrics.test.ts +3 -0
- data/lib/whatsapp_notifier/services/web_automation/metrics.ts +3 -0
- data/lib/whatsapp_notifier/services/web_automation/sessions.test.ts +22 -0
- data/lib/whatsapp_notifier/services/web_automation/sessions.ts +19 -0
- data/lib/whatsapp_notifier/version.rb +1 -1
- data/lib/whatsapp_notifier/web_adapter.rb +6 -2
- data/lib/whatsapp_notifier.rb +11 -0
- data/spec/error_code_spec.rb +107 -0
- data/spec/providers/web_automation_spec.rb +62 -0
- data/spec/session_ready_spec.rb +114 -0
- data/spec/web_adapter_spec.rb +35 -0
- metadata +5 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5516496d221334794f99898b801a9277c7e056971198cc7cb6577df6499c0d72
|
|
4
|
+
data.tar.gz: 2e06a216928933ba02bef20de6be794d089205effcd76ae5784d992776263fa8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 256eecd211ade964aca1453635dc0ca3a55f9d422473bf9fe26d52f793f429e4d78b6071b9906da749c1c2fd983a4ea3047f58b545a758421d9c819c55b03b4a
|
|
7
|
+
data.tar.gz: a0f2dca29b80d6d39b9d65ab7b924e31f5e4c39fcfc2ff8f0546514671ae4089e0131b30564338fc0f8dccc3d5ee1af691cf2903d272a7b565fea8fa04894882
|
data/README.md
CHANGED
|
@@ -1,88 +1,246 @@
|
|
|
1
|
-
#
|
|
1
|
+
# whatsapp_notifier
|
|
2
|
+
|
|
3
|
+
[](https://rubygems.org/gems/whatsapp_notifier)
|
|
4
|
+
[](https://github.com/kshtzkr/whatsapp_notifier/actions/workflows/ci.yml)
|
|
5
|
+
[](LICENSE.txt)
|
|
6
|
+
[](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
|
-
|
|
4
|
-
Set it up in minutes with one install generator and one service command.
|
|
74
|
+
## Installation
|
|
5
75
|
|
|
6
|
-
|
|
76
|
+
Add the gem:
|
|
7
77
|
|
|
8
|
-
|
|
78
|
+
```ruby
|
|
79
|
+
# Gemfile
|
|
80
|
+
gem "whatsapp_notifier"
|
|
81
|
+
```
|
|
9
82
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27
|
-
- scan QR
|
|
28
|
-
- send a test message
|
|
101
|
+
The gem ships no migrations, so there is no `db:migrate` step.
|
|
29
102
|
|
|
30
|
-
|
|
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
|
|
107
|
+
bundle exec whatsapp_notifier service
|
|
34
108
|
```
|
|
35
109
|
|
|
36
|
-
|
|
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
|
-
|
|
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
|
|
44
|
-
bin/rails g whatsapp_notifier:install
|
|
136
|
+
bundle exec whatsapp_notifier doctor
|
|
45
137
|
```
|
|
46
138
|
|
|
47
|
-
##
|
|
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: "
|
|
158
|
+
# => { state: "AUTHENTICATED", authenticated: true, has_qr: false }
|
|
55
159
|
```
|
|
56
160
|
|
|
57
|
-
|
|
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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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-ready — an unreachable status endpoint means the service is down,
|
|
168
|
+
so the send could not have succeeded either:
|
|
65
169
|
|
|
66
170
|
```ruby
|
|
67
|
-
WhatsAppNotifier.
|
|
68
|
-
# =>
|
|
171
|
+
WhatsAppNotifier.session_ready?(user_id: current_user.id)
|
|
172
|
+
# => true / false, never raises on a network error
|
|
69
173
|
```
|
|
70
174
|
|
|
71
|
-
|
|
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
|
-
##
|
|
182
|
+
## Sending a message
|
|
74
183
|
|
|
75
184
|
```ruby
|
|
76
185
|
result = WhatsAppNotifier.deliver(
|
|
77
186
|
to: "+919999999999",
|
|
78
|
-
body: "
|
|
187
|
+
body: "Payment received",
|
|
79
188
|
metadata: { user_id: current_user.id }
|
|
80
189
|
)
|
|
190
|
+
```
|
|
81
191
|
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
296
|
+
# => { total: 2, success: 2, failed: 0, results: [#<Result>, #<Result>] }
|
|
117
297
|
```
|
|
118
298
|
|
|
119
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
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
|
|
479
|
+
Released under the MIT License. See [LICENSE.txt](LICENSE.txt).
|