whatsapp_notifier 0.2.0
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 +7 -0
- data/Gemfile +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +126 -0
- data/Rakefile +6 -0
- data/bin/whatsapp_notifier +53 -0
- data/docs/bulk_messaging_policy.md +30 -0
- data/docs/graphify.md +108 -0
- data/docs/rails_setup.md +57 -0
- data/examples/notification_example.rb +14 -0
- data/lib/generators/whatsapp_notifier/install_generator.rb +60 -0
- data/lib/generators/whatsapp_notifier/install_service_generator.rb +33 -0
- data/lib/generators/whatsapp_notifier/templates/whatsapp_notifier.rb +6 -0
- data/lib/whatsapp_notifier/bulk/dispatcher.rb +64 -0
- data/lib/whatsapp_notifier/bulk/rate_limiter.rb +17 -0
- data/lib/whatsapp_notifier/bulk/retry_policy.rb +32 -0
- data/lib/whatsapp_notifier/client.rb +43 -0
- data/lib/whatsapp_notifier/configuration.rb +41 -0
- data/lib/whatsapp_notifier/doctor.rb +103 -0
- data/lib/whatsapp_notifier/errors.rb +5 -0
- data/lib/whatsapp_notifier/jobs/send_message_job.rb +20 -0
- data/lib/whatsapp_notifier/notification.rb +93 -0
- data/lib/whatsapp_notifier/providers/base.rb +24 -0
- data/lib/whatsapp_notifier/providers/web_automation.rb +85 -0
- data/lib/whatsapp_notifier/railtie.rb +14 -0
- data/lib/whatsapp_notifier/result.rb +23 -0
- data/lib/whatsapp_notifier/services/web_automation/bun.lock +452 -0
- data/lib/whatsapp_notifier/services/web_automation/index.ts +285 -0
- data/lib/whatsapp_notifier/services/web_automation/package.json +14 -0
- data/lib/whatsapp_notifier/session/qr_service.rb +51 -0
- data/lib/whatsapp_notifier/session/store.rb +22 -0
- data/lib/whatsapp_notifier/version.rb +4 -0
- data/lib/whatsapp_notifier/web_adapter.rb +72 -0
- data/lib/whatsapp_notifier.rb +72 -0
- data/spec/bulk/dispatcher_spec.rb +73 -0
- data/spec/bulk/rate_limiter_spec.rb +27 -0
- data/spec/bulk/retry_policy_spec.rb +33 -0
- data/spec/client_spec.rb +52 -0
- data/spec/configuration_spec.rb +47 -0
- data/spec/doctor_spec.rb +46 -0
- data/spec/jobs/send_message_job_spec.rb +36 -0
- data/spec/notification_spec.rb +60 -0
- data/spec/providers/base_spec.rb +17 -0
- data/spec/providers/web_automation_spec.rb +109 -0
- data/spec/railtie_spec.rb +37 -0
- data/spec/result_spec.rb +12 -0
- data/spec/session/qr_service_spec.rb +42 -0
- data/spec/session/store_spec.rb +21 -0
- data/spec/spec_helper.rb +17 -0
- data/spec/web_adapter_spec.rb +55 -0
- data/spec/whatsapp_notifier_spec.rb +102 -0
- metadata +126 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: '039a2704065400307efc4ba87239421078f1b63f97055c025ec226c6d26d907a'
|
|
4
|
+
data.tar.gz: 1af1676a7258d3c34e63d4fd2a6f7ab5ddb0158f78403c8d9ea65f2ccc54eda2
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: e7d97f32003bbdcd29e05a59857a1fc5e33c980b78a9aa6b4c0fa7398e44ce508ca629ad27ba8066759b538e4044a47904dfedfd9e74749a11dedc8de5fb0e4a
|
|
7
|
+
data.tar.gz: 4832f03b7c735afc1d735ff3ad1cfbd9717912a63e06defddbe3d0a7f4a1954b49e4e811b6597fcc61b573556e6cec636e572f5014beae7355aa578c6d2c2d7b
|
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kshitiz Sinha
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
# WhatsAppNotifier
|
|
2
|
+
|
|
3
|
+
`whatsapp_notifier` is a Rails-friendly gem for WhatsApp Web automation.
|
|
4
|
+
It is intentionally simple: run the bundled Bun service, scan a QR code, and send messages.
|
|
5
|
+
|
|
6
|
+
No official WhatsApp API setup, app review, or Meta webhook configuration is required.
|
|
7
|
+
|
|
8
|
+
## What You Get
|
|
9
|
+
|
|
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]`
|
|
15
|
+
|
|
16
|
+
## Quick Start (60 seconds)
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
bundle add whatsapp_notifier
|
|
20
|
+
bin/rails g whatsapp_notifier:install
|
|
21
|
+
bin/dev
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Then:
|
|
25
|
+
|
|
26
|
+
- open `/dashboard/whatsapp/qr`
|
|
27
|
+
- scan QR
|
|
28
|
+
- send a test message
|
|
29
|
+
|
|
30
|
+
If setup fails, run:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
bundle exec whatsapp_notifier doctor
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## Installation
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
gem "whatsapp_notifier"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
bundle install
|
|
44
|
+
bin/rails g whatsapp_notifier:install
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## Scan QR and check status
|
|
48
|
+
|
|
49
|
+
```ruby
|
|
50
|
+
# Use current_user.id for multi-user apps; omit metadata for a default shared session.
|
|
51
|
+
qr_data_url = WhatsAppNotifier.scan_qr(metadata: { user_id: current_user.id })
|
|
52
|
+
|
|
53
|
+
status = WhatsAppNotifier.connection_status(metadata: { user_id: current_user.id })
|
|
54
|
+
# => { state: "...", authenticated: true/false, has_qr: true/false }
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Send a message
|
|
58
|
+
|
|
59
|
+
```ruby
|
|
60
|
+
result = WhatsAppNotifier.deliver(
|
|
61
|
+
to: "+919999999999",
|
|
62
|
+
body: "Booking confirmed",
|
|
63
|
+
metadata: { user_id: current_user.id }
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
result.success?
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Notifications API
|
|
70
|
+
|
|
71
|
+
```ruby
|
|
72
|
+
class LeadWhatsappNotification < WhatsAppNotifier::Notification
|
|
73
|
+
def to
|
|
74
|
+
params[:lead].phone_number
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def message
|
|
78
|
+
"Hi #{params[:lead].name}, we are working on your itinerary."
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def metadata
|
|
82
|
+
{ user_id: params[:user].id }
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
```ruby
|
|
88
|
+
LeadWhatsappNotification.with(lead: @lead, user: current_user).deliver_later
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Bulk Messaging
|
|
92
|
+
|
|
93
|
+
```ruby
|
|
94
|
+
messages = [
|
|
95
|
+
{ to: "+919999999991", body: "Hello A", metadata: { user_id: current_user.id } },
|
|
96
|
+
{ to: "+919999999992", body: "Hello B", metadata: { user_id: current_user.id } }
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
summary = WhatsAppNotifier.deliver_bulk(messages)
|
|
100
|
+
summary[:success]
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
## Generators
|
|
104
|
+
|
|
105
|
+
Install everything with one command:
|
|
106
|
+
|
|
107
|
+
```bash
|
|
108
|
+
rails generate whatsapp_notifier:install
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
If you want to eject Bun service files into your app:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
rails generate whatsapp_notifier:install_service
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
This copies the service to `whatsapp_service/` and updates `.gitignore`.
|
|
118
|
+
|
|
119
|
+
## Notes
|
|
120
|
+
|
|
121
|
+
- This gem uses WhatsApp Web automation. Use responsibly and follow WhatsApp policies.
|
|
122
|
+
- Keep Chromium available in your runtime (or set `PUPPETEER_EXECUTABLE_PATH`).
|
|
123
|
+
|
|
124
|
+
## License
|
|
125
|
+
|
|
126
|
+
MIT. See `LICENSE.txt`.
|
data/Rakefile
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
|
|
3
|
+
require "thor"
|
|
4
|
+
require "whatsapp_notifier"
|
|
5
|
+
|
|
6
|
+
module WhatsAppNotifier
|
|
7
|
+
class CLI < Thor
|
|
8
|
+
def self.exit_on_failure?
|
|
9
|
+
true
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
desc "doctor", "Validate local setup and print exact fixes"
|
|
13
|
+
def doctor
|
|
14
|
+
ok = WhatsAppNotifier::Doctor.run
|
|
15
|
+
raise Thor::Error, "Doctor checks failed. Apply fixes above and run again." unless ok
|
|
16
|
+
|
|
17
|
+
puts "All checks passed."
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
desc "service", "Start the WhatsApp automation service (Bun)"
|
|
21
|
+
option :port, type: :numeric, default: WhatsAppNotifier::Doctor::DEFAULT_PORT, desc: "Port to run the service on"
|
|
22
|
+
def service
|
|
23
|
+
service_path = WhatsAppNotifier.service_path
|
|
24
|
+
puts "Starting WhatsApp Notifier Service from #{service_path}..."
|
|
25
|
+
|
|
26
|
+
ENV["PORT"] ||= options[:port].to_s
|
|
27
|
+
ENV["WHATSAPP_SESSION_DIR"] ||= WhatsAppNotifier::Doctor.session_dir
|
|
28
|
+
|
|
29
|
+
unless system("bun --version > /dev/null 2>&1")
|
|
30
|
+
raise Thor::Error, "Bun is required. Install from https://bun.sh"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Auto-detect Chromium in common locations for development
|
|
34
|
+
if !ENV["PUPPETEER_EXECUTABLE_PATH"] && File.exist?("/usr/bin/chromium")
|
|
35
|
+
ENV["PUPPETEER_EXECUTABLE_PATH"] = "/usr/bin/chromium"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
Dir.chdir(service_path) do
|
|
39
|
+
# Ensure dependencies are installed
|
|
40
|
+
unless File.exist?("node_modules")
|
|
41
|
+
puts "Installing dependencies (bun install)..."
|
|
42
|
+
unless system("bun install")
|
|
43
|
+
raise Thor::Error, "Failed to install Bun dependencies in #{service_path}"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
exec("bun index.ts")
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
WhatsAppNotifier::CLI.start(ARGV)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# Bulk Messaging Policy Guardrails
|
|
2
|
+
|
|
3
|
+
This gem includes technical controls to reduce policy and spam risks:
|
|
4
|
+
|
|
5
|
+
- paced delivery via `bulk_base_delay_seconds` and `bulk_jitter_seconds`
|
|
6
|
+
- capped recipients via `bulk_max_recipients`
|
|
7
|
+
- bounded retries via `bulk_max_attempts`
|
|
8
|
+
- retry only on configured transient error codes
|
|
9
|
+
- automatic wait/sleep when provider returns `wait_seconds`
|
|
10
|
+
- idempotency key deduplication in the same bulk run
|
|
11
|
+
|
|
12
|
+
## Recommended defaults
|
|
13
|
+
|
|
14
|
+
- keep throughput conservative and user-scoped (`metadata[:user_id]`)
|
|
15
|
+
- avoid unsolicited messaging
|
|
16
|
+
- use explicit opt-in recipient lists
|
|
17
|
+
|
|
18
|
+
## Wait-time compliance
|
|
19
|
+
|
|
20
|
+
If provider returns:
|
|
21
|
+
|
|
22
|
+
```ruby
|
|
23
|
+
{ success: false, error_code: :rate_limited, wait_seconds: 30 }
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
the bulk dispatcher pauses for 30 seconds before moving forward.
|
|
27
|
+
|
|
28
|
+
## Web automation warning
|
|
29
|
+
|
|
30
|
+
This gem uses `:web_automation`. Keep use-cases controlled, compliant, and consent-based.
|
data/docs/graphify.md
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Graphify this repository
|
|
2
|
+
|
|
3
|
+
[Graphify](https://github.com/sponsors/safishamsi) turns a folder of code and docs into a **persistent knowledge graph**: entities, relationships (tagged as extracted vs inferred), community clusters, and browsable outputs. Use it when you want a map of `whatsapp_notifier` before refactoring, onboarding, or tracing how bulk delivery, providers, and Rails integration connect.
|
|
4
|
+
|
|
5
|
+
This document describes how to run the pipeline **from the gem root** (`whatsapp_notifier/`). It mirrors the `/graphify` agent workflow; you can also run the same steps in a terminal.
|
|
6
|
+
|
|
7
|
+
## Prerequisites
|
|
8
|
+
|
|
9
|
+
- **Python 3** with pip
|
|
10
|
+
- Install the package (the CLI may be published as `graphifyy`):
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
python3 -m pip install graphifyy
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
- Optional: `graphify` on your `PATH` (the installer may expose it). If missing, use `python3 -m` forms below.
|
|
17
|
+
|
|
18
|
+
## What gets indexed here
|
|
19
|
+
|
|
20
|
+
Reasonable targets for this repo:
|
|
21
|
+
|
|
22
|
+
| Path | Role |
|
|
23
|
+
|------|------|
|
|
24
|
+
| `.` (repo root) | Full corpus: `lib/`, `spec/`, `docs/`, `examples/` |
|
|
25
|
+
| `lib/whatsapp_notifier` | Core implementation only (smaller graph) |
|
|
26
|
+
| `docs/` | Policy and setup prose only |
|
|
27
|
+
|
|
28
|
+
Ruby (`.rb`) is treated as **code** (AST + optional semantic extraction). Markdown under `docs/` is **documents**.
|
|
29
|
+
|
|
30
|
+
## One-shot run (full pipeline)
|
|
31
|
+
|
|
32
|
+
From the repository root:
|
|
33
|
+
|
|
34
|
+
1. **Ensure graphify is importable** and record the interpreter (so later steps use the same venv):
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
mkdir -p graphify-out
|
|
38
|
+
python3 -c "import graphify" 2>/dev/null || python3 -m pip install graphifyy -q
|
|
39
|
+
python3 -c "import sys; open('graphify-out/.graphify_python', 'w').write(sys.executable)"
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
2. **Detect files** (writes `graphify-out/.graphify_detect.json`; inspect with a JSON viewer or small script if you need counts).
|
|
43
|
+
|
|
44
|
+
3. **Extract**
|
|
45
|
+
- **Code:** AST extraction over Ruby files (imports/calls structure).
|
|
46
|
+
- **Docs / non-code:** semantic extraction (LLM) unless the corpus is code-only.
|
|
47
|
+
For large corpora, graphify supports **caching** and **chunked** extraction; see upstream docs.
|
|
48
|
+
|
|
49
|
+
4. **Build graph, cluster, report** — produces the main artifacts below.
|
|
50
|
+
|
|
51
|
+
5. **Label communities** — short human names per cluster, then regenerate the report.
|
|
52
|
+
|
|
53
|
+
6. **HTML** — interactive `graphify-out/graph.html` (skipped or warned if the graph is huge).
|
|
54
|
+
|
|
55
|
+
7. **Cleanup** — some intermediate JSON files may be removed at the end of a full run; **`graph.json`**, **`GRAPH_REPORT.md`**, and **`graph.html`** are the durable outputs.
|
|
56
|
+
|
|
57
|
+
Use the interpreter pinned in `graphify-out/.graphify_python` for all `python -c "…"` snippets so a venv and the CLI stay consistent:
|
|
58
|
+
|
|
59
|
+
```bash
|
|
60
|
+
PY="$(cat graphify-out/.graphify_python)"
|
|
61
|
+
"$PY" -c "import graphify; print('ok')"
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
## Outputs (under `graphify-out/`)
|
|
65
|
+
|
|
66
|
+
After a successful run you should have:
|
|
67
|
+
|
|
68
|
+
| Output | Purpose |
|
|
69
|
+
|--------|---------|
|
|
70
|
+
| `graph.html` | Interactive graph in a browser (no server) |
|
|
71
|
+
| `GRAPH_REPORT.md` | Audit-style report: god nodes, surprising links, suggested questions, token usage |
|
|
72
|
+
| `graph.json` | GraphRAG-style structured graph for tools or custom queries |
|
|
73
|
+
| `cost.json` | Cumulative token accounting across runs (if enabled by your graphify version) |
|
|
74
|
+
|
|
75
|
+
Optional flags (when using the full `/graphify` agent or CLI with the same options):
|
|
76
|
+
|
|
77
|
+
- `--no-viz` — report + JSON only
|
|
78
|
+
- `--obsidian` / `--obsidian-dir` — Obsidian vault + canvas
|
|
79
|
+
- `--update` — incremental re-extraction for changed files only
|
|
80
|
+
- `--directed` — preserve edge direction in the graph
|
|
81
|
+
- `--svg` / `--graphml` / `--neo4j` — extra export formats
|
|
82
|
+
|
|
83
|
+
## Queries after the graph exists
|
|
84
|
+
|
|
85
|
+
With `graphify-out/graph.json` present, you can:
|
|
86
|
+
|
|
87
|
+
- **Broad context:** BFS-style “what is X connected to?”
|
|
88
|
+
- **Paths:** shortest path between two labeled concepts
|
|
89
|
+
- **Explain:** neighborhood of one node
|
|
90
|
+
|
|
91
|
+
The graphify CLI or agent subcommands (`query`, `path`, `explain`) operate on that file; answers should cite graph edges and confidence tags, not invent links.
|
|
92
|
+
|
|
93
|
+
## Large repo warning
|
|
94
|
+
|
|
95
|
+
If detection reports **very high** file or word counts, run graphify on a **subfolder** (for example `lib/whatsapp_notifier` only) first. That keeps HTML visualization and clustering responsive.
|
|
96
|
+
|
|
97
|
+
## CI and secrets
|
|
98
|
+
|
|
99
|
+
Do **not** commit API keys or production phone numbers into the corpus before indexing. This gem’s **specs and examples** should use placeholders; scrub any local overrides before running graphify on a copy of the tree if you use real data in dev.
|
|
100
|
+
|
|
101
|
+
## Related project docs
|
|
102
|
+
|
|
103
|
+
- [Rails setup](rails_setup.md) — initializer and Active Job wiring
|
|
104
|
+
- [Bulk messaging policy](bulk_messaging_policy.md) — rate limits and guardrails
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
*Workflow reference: Cursor `/graphify` skill (graph detection → AST + semantic merge → cluster → report → HTML). For the canonical flag list and agent-only steps (parallel semantic chunks, Whisper for video), use the skill or upstream graphify documentation.*
|
data/docs/rails_setup.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# Rails Setup
|
|
2
|
+
|
|
3
|
+
## Initializer
|
|
4
|
+
|
|
5
|
+
Create `config/initializers/whatsapp_notifier.rb`:
|
|
6
|
+
|
|
7
|
+
```ruby
|
|
8
|
+
WhatsAppNotifier.configure do |config|
|
|
9
|
+
config.provider = :web_automation
|
|
10
|
+
config.web_automation_enabled = true
|
|
11
|
+
config.bulk_base_delay_seconds = 1.2
|
|
12
|
+
config.bulk_jitter_seconds = 0.4
|
|
13
|
+
config.bulk_max_recipients = 300
|
|
14
|
+
config.bulk_max_attempts = 3
|
|
15
|
+
end
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Defaults already use a built-in adapter that talks to `http://127.0.0.1:3001`.
|
|
19
|
+
|
|
20
|
+
## Global usage
|
|
21
|
+
|
|
22
|
+
`WhatsAppNotifier` is available globally:
|
|
23
|
+
|
|
24
|
+
```ruby
|
|
25
|
+
WhatsAppNotifier.deliver(
|
|
26
|
+
to: "+919999999999",
|
|
27
|
+
body: "Payment received",
|
|
28
|
+
metadata: { user_id: current_user.id }
|
|
29
|
+
)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Mailer-like class usage
|
|
33
|
+
|
|
34
|
+
```ruby
|
|
35
|
+
class AlertsNotification < WhatsAppNotifier::Notification
|
|
36
|
+
to "+919999999999"
|
|
37
|
+
provider :web_automation
|
|
38
|
+
template :payment_alert, "Hi {{name}}, payment {{amount}} received."
|
|
39
|
+
|
|
40
|
+
def metadata
|
|
41
|
+
{ user_id: params[:user_id] }
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
AlertsNotification.deliver_now(params: { name: "Riya", amount: "INR 1500" }, user_id: 42)
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
## Async behavior
|
|
49
|
+
|
|
50
|
+
`deliver_later` requires ActiveJob to be available.
|
|
51
|
+
|
|
52
|
+
```ruby
|
|
53
|
+
AlertsNotification.deliver_later(
|
|
54
|
+
to: "+919999999999",
|
|
55
|
+
params: { name: "Riya", amount: "INR 1500" }
|
|
56
|
+
)
|
|
57
|
+
```
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
require "whatsapp_notifier"
|
|
2
|
+
|
|
3
|
+
WhatsAppNotifier.configure do |config|
|
|
4
|
+
config.provider = :web_automation
|
|
5
|
+
config.web_automation_enabled = true
|
|
6
|
+
end
|
|
7
|
+
|
|
8
|
+
class WelcomeNotification < WhatsAppNotifier::Notification
|
|
9
|
+
to "+919999999999"
|
|
10
|
+
provider :web_automation
|
|
11
|
+
template :welcome, "Hi {{name}}, welcome to our platform."
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
WelcomeNotification.deliver_now(params: { name: "Asha" }, metadata: { user_id: "demo-user" })
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
require "rails/generators"
|
|
2
|
+
require "whatsapp_notifier/doctor"
|
|
3
|
+
|
|
4
|
+
module WhatsAppNotifier
|
|
5
|
+
module Generators
|
|
6
|
+
class InstallGenerator < Rails::Generators::Base
|
|
7
|
+
source_root File.expand_path("templates", __dir__)
|
|
8
|
+
|
|
9
|
+
def create_initializer
|
|
10
|
+
return if File.exist?("config/initializers/whatsapp_notifier.rb")
|
|
11
|
+
|
|
12
|
+
template "whatsapp_notifier.rb", "config/initializers/whatsapp_notifier.rb"
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def ensure_procfile_entry
|
|
16
|
+
procfile = "Procfile.dev"
|
|
17
|
+
line = "whatsapp: bundle exec whatsapp_notifier service"
|
|
18
|
+
|
|
19
|
+
unless File.exist?(procfile)
|
|
20
|
+
create_file(procfile, "#{line}\n")
|
|
21
|
+
return
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
content = File.read(procfile)
|
|
25
|
+
return if content.lines.any? { |existing| existing.strip == line }
|
|
26
|
+
|
|
27
|
+
append_to_file(procfile, "\n#{line}\n")
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def ensure_gitignore_entries
|
|
31
|
+
entries = [
|
|
32
|
+
"# WhatsApp Notifier",
|
|
33
|
+
"/tmp/whatsapp_notifier",
|
|
34
|
+
"/whatsapp_service/node_modules",
|
|
35
|
+
"/whatsapp_service/.wwebjs_cache",
|
|
36
|
+
"/whatsapp_service/.wwebjs_auth"
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
content = File.exist?(".gitignore") ? File.read(".gitignore") : ""
|
|
40
|
+
missing = entries.reject { |entry| content.include?(entry) }
|
|
41
|
+
return if missing.empty?
|
|
42
|
+
|
|
43
|
+
append_to_file(".gitignore", "\n#{missing.join("\n")}\n")
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def run_doctor
|
|
47
|
+
say("\nRunning setup doctor...", :yellow)
|
|
48
|
+
ok = WhatsAppNotifier::Doctor.run(io: $stdout, app_root: destination_root)
|
|
49
|
+
return if ok
|
|
50
|
+
|
|
51
|
+
raise Thor::Error, "Setup checks failed. Fix items above, then run `bundle exec whatsapp_notifier doctor`."
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def next_steps
|
|
55
|
+
say("\nSetup complete.", :green)
|
|
56
|
+
say("Run `bin/dev`, open `/dashboard/whatsapp/qr`, then send a test message.", :green)
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
require 'rails/generators'
|
|
2
|
+
|
|
3
|
+
module WhatsAppNotifier
|
|
4
|
+
module Generators
|
|
5
|
+
class InstallServiceGenerator < Rails::Generators::Base
|
|
6
|
+
source_root File.expand_path("../../whatsapp_notifier/services/web_automation", __dir__)
|
|
7
|
+
|
|
8
|
+
def copy_service_files
|
|
9
|
+
directory '.', 'whatsapp_service'
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def add_to_gitignore
|
|
13
|
+
entries = [
|
|
14
|
+
"# WhatsApp Service",
|
|
15
|
+
"/whatsapp_service/node_modules",
|
|
16
|
+
"/whatsapp_service/.wwebjs_cache",
|
|
17
|
+
"/whatsapp_service/.wwebjs_auth"
|
|
18
|
+
]
|
|
19
|
+
content = File.exist?(".gitignore") ? File.read(".gitignore") : ""
|
|
20
|
+
missing_entries = entries.reject { |entry| content.include?(entry) }
|
|
21
|
+
return if missing_entries.empty?
|
|
22
|
+
|
|
23
|
+
append_to_file ".gitignore", "\n#{missing_entries.join("\n")}\n"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def show_readme
|
|
27
|
+
say "\nWhatsApp Service installed in /whatsapp_service", :green
|
|
28
|
+
say "Make sure you have Bun installed: https://bun.sh", :yellow
|
|
29
|
+
say "To start the service manually: cd whatsapp_service && bun index.ts", :yellow
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
require "set"
|
|
2
|
+
|
|
3
|
+
module WhatsAppNotifier
|
|
4
|
+
module Bulk
|
|
5
|
+
class Dispatcher
|
|
6
|
+
def initialize(client:, configuration:, sleeper: ->(seconds) { sleep(seconds) }, rng: Random.new)
|
|
7
|
+
@client = client
|
|
8
|
+
@configuration = configuration
|
|
9
|
+
@rate_limiter = RateLimiter.new(
|
|
10
|
+
base_delay: configuration.bulk_base_delay_seconds,
|
|
11
|
+
jitter: configuration.bulk_jitter_seconds,
|
|
12
|
+
sleeper: sleeper,
|
|
13
|
+
rng: rng
|
|
14
|
+
)
|
|
15
|
+
@retry_policy = RetryPolicy.new(
|
|
16
|
+
max_attempts: configuration.bulk_max_attempts,
|
|
17
|
+
retryable_error_codes: configuration.bulk_retryable_error_codes
|
|
18
|
+
)
|
|
19
|
+
@sleeper = sleeper
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def deliver(messages, provider: nil)
|
|
23
|
+
raise ConfigurationError, "messages must be an array" unless messages.is_a?(Array)
|
|
24
|
+
raise ConfigurationError, "bulk_max_recipients exceeded" if messages.length > @configuration.bulk_max_recipients
|
|
25
|
+
|
|
26
|
+
sent_keys = Set.new
|
|
27
|
+
results = messages.map.with_index do |message, idx|
|
|
28
|
+
@rate_limiter.wait_before_next if idx.positive?
|
|
29
|
+
deliver_one(message, sent_keys: sent_keys, provider: provider)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
{ total: results.length, success: results.count(&:success?), failed: results.count(&:failure?), results: results }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
private
|
|
36
|
+
|
|
37
|
+
def deliver_one(message, sent_keys:, provider:)
|
|
38
|
+
idempotency_key = message[:idempotency_key]
|
|
39
|
+
if idempotency_key && sent_keys.include?(idempotency_key)
|
|
40
|
+
return Result.new(
|
|
41
|
+
success: false,
|
|
42
|
+
provider: provider || @configuration.provider,
|
|
43
|
+
error_code: :duplicate_idempotency_key,
|
|
44
|
+
error_message: "idempotency key already processed"
|
|
45
|
+
)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
result = @retry_policy.with_retries do |_attempt|
|
|
49
|
+
@client.deliver(
|
|
50
|
+
to: message.fetch(:to),
|
|
51
|
+
body: message.fetch(:body),
|
|
52
|
+
metadata: message.fetch(:metadata, {}),
|
|
53
|
+
provider: provider,
|
|
54
|
+
idempotency_key: idempotency_key
|
|
55
|
+
)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
@sleeper.call(result.wait_seconds) if result.wait_seconds.to_f.positive?
|
|
59
|
+
sent_keys << idempotency_key if idempotency_key && result.success?
|
|
60
|
+
result
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
module WhatsAppNotifier
|
|
2
|
+
module Bulk
|
|
3
|
+
class RateLimiter
|
|
4
|
+
def initialize(base_delay:, jitter:, sleeper:, rng: Random.new)
|
|
5
|
+
@base_delay = base_delay.to_f
|
|
6
|
+
@jitter = jitter.to_f
|
|
7
|
+
@sleeper = sleeper
|
|
8
|
+
@rng = rng
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def wait_before_next
|
|
12
|
+
delay = @base_delay + (@rng.rand * @jitter)
|
|
13
|
+
@sleeper.call(delay) if delay.positive?
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module WhatsAppNotifier
|
|
2
|
+
module Bulk
|
|
3
|
+
class RetryPolicy
|
|
4
|
+
def initialize(max_attempts:, retryable_error_codes:)
|
|
5
|
+
@max_attempts = max_attempts
|
|
6
|
+
@retryable_error_codes = retryable_error_codes
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
def with_retries
|
|
10
|
+
attempts = 0
|
|
11
|
+
result = nil
|
|
12
|
+
|
|
13
|
+
loop do
|
|
14
|
+
attempts += 1
|
|
15
|
+
result = yield(attempts)
|
|
16
|
+
break unless retry?(result, attempts)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
result
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def retry?(result, attempts)
|
|
25
|
+
return false if attempts >= @max_attempts
|
|
26
|
+
return false if result.success?
|
|
27
|
+
|
|
28
|
+
@retryable_error_codes.include?(result.error_code)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|