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.
Files changed (52) hide show
  1. checksums.yaml +7 -0
  2. data/Gemfile +7 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +126 -0
  5. data/Rakefile +6 -0
  6. data/bin/whatsapp_notifier +53 -0
  7. data/docs/bulk_messaging_policy.md +30 -0
  8. data/docs/graphify.md +108 -0
  9. data/docs/rails_setup.md +57 -0
  10. data/examples/notification_example.rb +14 -0
  11. data/lib/generators/whatsapp_notifier/install_generator.rb +60 -0
  12. data/lib/generators/whatsapp_notifier/install_service_generator.rb +33 -0
  13. data/lib/generators/whatsapp_notifier/templates/whatsapp_notifier.rb +6 -0
  14. data/lib/whatsapp_notifier/bulk/dispatcher.rb +64 -0
  15. data/lib/whatsapp_notifier/bulk/rate_limiter.rb +17 -0
  16. data/lib/whatsapp_notifier/bulk/retry_policy.rb +32 -0
  17. data/lib/whatsapp_notifier/client.rb +43 -0
  18. data/lib/whatsapp_notifier/configuration.rb +41 -0
  19. data/lib/whatsapp_notifier/doctor.rb +103 -0
  20. data/lib/whatsapp_notifier/errors.rb +5 -0
  21. data/lib/whatsapp_notifier/jobs/send_message_job.rb +20 -0
  22. data/lib/whatsapp_notifier/notification.rb +93 -0
  23. data/lib/whatsapp_notifier/providers/base.rb +24 -0
  24. data/lib/whatsapp_notifier/providers/web_automation.rb +85 -0
  25. data/lib/whatsapp_notifier/railtie.rb +14 -0
  26. data/lib/whatsapp_notifier/result.rb +23 -0
  27. data/lib/whatsapp_notifier/services/web_automation/bun.lock +452 -0
  28. data/lib/whatsapp_notifier/services/web_automation/index.ts +285 -0
  29. data/lib/whatsapp_notifier/services/web_automation/package.json +14 -0
  30. data/lib/whatsapp_notifier/session/qr_service.rb +51 -0
  31. data/lib/whatsapp_notifier/session/store.rb +22 -0
  32. data/lib/whatsapp_notifier/version.rb +4 -0
  33. data/lib/whatsapp_notifier/web_adapter.rb +72 -0
  34. data/lib/whatsapp_notifier.rb +72 -0
  35. data/spec/bulk/dispatcher_spec.rb +73 -0
  36. data/spec/bulk/rate_limiter_spec.rb +27 -0
  37. data/spec/bulk/retry_policy_spec.rb +33 -0
  38. data/spec/client_spec.rb +52 -0
  39. data/spec/configuration_spec.rb +47 -0
  40. data/spec/doctor_spec.rb +46 -0
  41. data/spec/jobs/send_message_job_spec.rb +36 -0
  42. data/spec/notification_spec.rb +60 -0
  43. data/spec/providers/base_spec.rb +17 -0
  44. data/spec/providers/web_automation_spec.rb +109 -0
  45. data/spec/railtie_spec.rb +37 -0
  46. data/spec/result_spec.rb +12 -0
  47. data/spec/session/qr_service_spec.rb +42 -0
  48. data/spec/session/store_spec.rb +21 -0
  49. data/spec/spec_helper.rb +17 -0
  50. data/spec/web_adapter_spec.rb +55 -0
  51. data/spec/whatsapp_notifier_spec.rb +102 -0
  52. 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
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ gem "rake", "~> 13.0"
6
+ gem "rspec", "~> 3.13"
7
+ gem "simplecov", "~> 0.22", require: false
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,6 @@
1
+ require "rake/testtask"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task default: :spec
@@ -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.*
@@ -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,6 @@
1
+ WhatsAppNotifier.configure do |config|
2
+ config.provider = :web_automation
3
+ config.web_automation_enabled = true
4
+ end
5
+
6
+ ENV["WHATSAPP_NOTIFIER_SERVICE_URL"] ||= "http://127.0.0.1:3001"
@@ -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