botforje 0.0.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.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +381 -0
  3. package/dist/action/action.js +44 -0
  4. package/dist/action/catalog.js +40 -0
  5. package/dist/action/cooldown.js +60 -0
  6. package/dist/action/executor.js +23 -0
  7. package/dist/action/template.js +13 -0
  8. package/dist/action/types.js +2 -0
  9. package/dist/action/webhook.js +36 -0
  10. package/dist/actions/action.js +26 -0
  11. package/dist/actions/cooldown.js +66 -0
  12. package/dist/actions/request.js +40 -0
  13. package/dist/api/routes/auth.js +56 -0
  14. package/dist/api/routes/bots.js +51 -0
  15. package/dist/api/routes/config.js +24 -0
  16. package/dist/api/routes/health.js +15 -0
  17. package/dist/api/routes/index.js +14 -0
  18. package/dist/api/routes/messages.js +69 -0
  19. package/dist/api/routes/sessions.js +97 -0
  20. package/dist/api/routes/status.js +38 -0
  21. package/dist/api/server.js +153 -0
  22. package/dist/auth/service.js +102 -0
  23. package/dist/boot/fleet.js +207 -0
  24. package/dist/bot/bot.js +30 -0
  25. package/dist/bot/fleet.js +211 -0
  26. package/dist/bot/fuzzy.js +20 -0
  27. package/dist/bot/mapper.js +47 -0
  28. package/dist/bot/types.js +2 -0
  29. package/dist/bot/validation.js +38 -0
  30. package/dist/bot.js +31 -0
  31. package/dist/cli/create-bot.js +84 -0
  32. package/dist/cli.js +138 -0
  33. package/dist/commands/auth.js +200 -0
  34. package/dist/commands/create-bot.js +64 -0
  35. package/dist/commands/guide.js +563 -0
  36. package/dist/commands/lock.js +73 -0
  37. package/dist/commands/status.js +145 -0
  38. package/dist/commands/unlock.js +88 -0
  39. package/dist/commands/validate.js +19 -0
  40. package/dist/config/mapper.js +152 -0
  41. package/dist/config/schema.js +2 -0
  42. package/dist/config/validation.js +705 -0
  43. package/dist/config/watcher.js +145 -0
  44. package/dist/config/yaml.js +197 -0
  45. package/dist/fleet.js +247 -0
  46. package/dist/flow/executor.js +286 -0
  47. package/dist/flow/flow.js +2 -0
  48. package/dist/flow/mapper.js +72 -0
  49. package/dist/flow/state.js +115 -0
  50. package/dist/flow/types.js +2 -0
  51. package/dist/graph/executor.js +294 -0
  52. package/dist/graph/graph.js +2 -0
  53. package/dist/graph/state.js +118 -0
  54. package/dist/helpers/data.js +42 -0
  55. package/dist/helpers/fuzzy.js +30 -0
  56. package/dist/helpers/logger.js +89 -0
  57. package/dist/helpers/validation.js +38 -0
  58. package/dist/index.js +92 -0
  59. package/dist/messages/contracts.js +2 -0
  60. package/dist/messages/inbox.js +58 -0
  61. package/dist/messages/outbox.js +136 -0
  62. package/dist/services/auto-response.js +62 -0
  63. package/dist/services/cooldown.js +60 -0
  64. package/dist/services/fleet.js +207 -0
  65. package/dist/services/flow-executor.js +195 -0
  66. package/dist/services/flow-state.js +118 -0
  67. package/dist/services/inbox.js +50 -0
  68. package/dist/services/message-handler.js +78 -0
  69. package/dist/services/message-queue.js +138 -0
  70. package/dist/services/outbox.js +138 -0
  71. package/dist/services/session.js +118 -0
  72. package/dist/services/webhook.js +87 -0
  73. package/dist/utils/logger.js +89 -0
  74. package/dist/utils/webhook.js +36 -0
  75. package/dist/validation/validate.js +592 -0
  76. package/dist/whatsapp/client.js +293 -0
  77. package/dist/whatsapp/session.js +158 -0
  78. package/dist/whatsapp/types.js +109 -0
  79. package/dist/whatsapp/whatsapp.js +109 -0
  80. package/package.json +97 -0
  81. package/scripts/postinstall.js +45 -0
  82. package/scripts/preuninstall.js +48 -0
  83. package/scripts/setup-systemd.js +91 -0
  84. package/service/botforje.service.template +25 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 felixzsh (Félix Sánchez)
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.
package/README.md ADDED
@@ -0,0 +1,381 @@
1
+ # Botforje
2
+
3
+ [![npm version](https://img.shields.io/npm/v/botforje.svg)](https://www.npmjs.com/package/botforje)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+ [![Node.js Version](https://img.shields.io/badge/node-%3E%3D16-brightgreen.svg)](https://nodejs.org)
6
+ [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/felixzsh/botforje/pulls)
7
+
8
+ **Create multiple WhatsApp bots without writing code** - Just configure in YAML!
9
+
10
+ Botforje lets you create and manage multiple WhatsApp bots by simply editing a configuration file. No programming required! Built on top of [WhatsApp Web JS](https://github.com/pedroslopez/whatsapp-web.js).
11
+
12
+ ## What Can You Do?
13
+
14
+ - **Multiple Bots**: Run several WhatsApp bots from one server
15
+ - **YAML Configuration**: Define bot behavior in simple YAML files
16
+ - **Actions & Graphs**: Build conversation state machines with fuzzy-matched edges
17
+ - **Requests**: Connect to your existing apps via HTTP
18
+ - **REST API**: Send messages programmatically (optional)
19
+ - **Systemd Service**: Run as a proper system service with auto-restart
20
+
21
+ ## Quick Start
22
+
23
+ ### Prerequisites
24
+
25
+ **System Requirements:**
26
+ - **Node.js** 22.13+ and **npm/pnpm**
27
+ - **Chromium browser** installed on your system
28
+ - **For Linux systemd service mode**: `xvfb` (X Virtual Framebuffer) for headless operation
29
+
30
+
31
+ ### 1. Install
32
+
33
+ ```bash
34
+ npm install -g botforje
35
+ ```
36
+
37
+ ### 2. Setup & Start as System Service
38
+
39
+ The daemon is configured automatically during install. Start it with:
40
+
41
+ ```bash
42
+ # Start the service
43
+ systemctl --user start botforje
44
+
45
+ # Enable auto-start on boot
46
+ systemctl --user enable botforje
47
+ ```
48
+
49
+ ### 3. Authenticate Your Bot
50
+
51
+ ```bash
52
+ botforje auth <botId>
53
+ ```
54
+
55
+ Shows a QR code to link your WhatsApp account. Once scanned, the bot is authenticated and ready.
56
+
57
+ ### 4. Check Status
58
+
59
+ ```bash
60
+ botforje status
61
+ journalctl --user -u botforje -f # real-time logs
62
+ ```
63
+
64
+ ## Configuration Guide
65
+
66
+ ### Global Configuration
67
+
68
+ Configure system-wide settings at the top level of `config.yml`:
69
+
70
+ ```yaml
71
+ chromium_path: "/usr/bin/chromium" # Path to Chromium/Chrome browser
72
+ port: 3000 # REST API port (set to enable the API)
73
+ address: "127.0.0.1" # Bind address (127.0.0.1 = localhost only, 0.0.0.0 = all interfaces)
74
+ log_level: "info" # Global log level
75
+ default_timeout: 300 # Global default timeout for graph sessions (seconds)
76
+ ```
77
+
78
+ ### Architecture
79
+
80
+ Botforje uses three catalogs — **Actions**, **Graphs**, and **Bots** — all defined in a single YAML map:
81
+
82
+ - **Actions**: Reusable behaviors — text replies, request calls, cooldowns. Not tied to any specific bot.
83
+ - **Graphs**: Conversation state machines. A graph owns a set of nodes connected by fuzzy-matched edges. Each bot references exactly one graph.
84
+ - **Bots**: WhatsApp numbers that reference a single graph and have per-bot settings.
85
+
86
+ ```
87
+ Bot
88
+ └─ Graph (one per bot)
89
+ └─ Nodes
90
+ └─ Edges (transitions based on fuzzy-matched user input)
91
+ └─ Actions
92
+ ```
93
+
94
+ ### Actions
95
+
96
+ Actions are the building blocks, defined as a **pipeline of ordered steps**. Each action can have an optional `guards` block (for rate-limiting) and a `steps` array that runs in sequence.
97
+
98
+ Replies support template variables:
99
+ - `{{senderPhone}}` — the sender's phone number
100
+ - `{{message}}` — the incoming message text
101
+ - `{{bot.id}}` — the bot's ID
102
+ - `{{variables.name}}` — graph session variables
103
+
104
+ ```yaml
105
+ actions:
106
+ greet:
107
+ steps:
108
+ - message:
109
+ body: "Hello! How can I help you?"
110
+
111
+ escalate:
112
+ guards:
113
+ cooldown:
114
+ duration: 120
115
+ on_blocked:
116
+ - message:
117
+ body: "You already requested a human agent. Please wait."
118
+ steps:
119
+ - message:
120
+ body: "Connecting you to a human agent."
121
+ - request:
122
+ name: escalate-human
123
+ url: "https://api.example.com/support/escalate"
124
+ method: POST
125
+ headers:
126
+ Authorization: "Bearer your-api-token"
127
+ timeout: 10000
128
+ retry: 3
129
+
130
+ lead-notify:
131
+ steps:
132
+ - request:
133
+ name: lead-capture
134
+ url: "https://crm.example.com/leads"
135
+ method: POST
136
+ headers:
137
+ X-API-Key: "your-crm-key"
138
+
139
+ # Send a WhatsApp location pin (combined with a text message)
140
+ send-office:
141
+ steps:
142
+ - message:
143
+ body: "Here is our office."
144
+ - location:
145
+ latitude: 19.4326
146
+ longitude: -99.1332
147
+ name: "Main Office"
148
+ address: "Av. Reforma 123, CDMX"
149
+ url: "https://maps.example.com/office"
150
+ description: "Open Mon-Fri 9-18h"
151
+
152
+ # Location-only action (no text message)
153
+ send-store-only:
154
+ steps:
155
+ - location:
156
+ latitude: 19.4326
157
+ longitude: -99.1332
158
+ name: "Store"
159
+ ```
160
+
161
+ **Step types:**
162
+
163
+ | Step | Description |
164
+ |---|---|
165
+ | `message` | Sends a text message (optionally to a different recipient via `to`) |
166
+ | `request` | Fires an HTTP request |
167
+ | `location` | Sends a WhatsApp location pin |
168
+
169
+ When a request fires, it sends a JSON payload:
170
+
171
+ ```json
172
+ {
173
+ "senderPhone": "521234567890",
174
+ "senderName": "John Doe",
175
+ "message": "I'm interested in your product",
176
+ "timestamp": "2025-01-09T01:45:00Z",
177
+ "botId": "support-bot",
178
+ "botName": "support-bot",
179
+ "requestName": "lead-capture",
180
+ "metadata": {}
181
+ }
182
+ ```
183
+
184
+ ### Graphs
185
+
186
+ Graphs define multi-step conversations. Each graph has a `root` node, optional `timeout`, optional `fallback` for unmatched input, and a map of `nodes` connected by `edges`.
187
+
188
+ ```yaml
189
+ graphs:
190
+ faq-support:
191
+ root: menu
192
+ timeout: 300 # Session TTL (seconds) — session dies after inactivity
193
+ fallback: invalid # Where to go if user sends an unexpected response
194
+ nodes:
195
+ menu:
196
+ action: menu
197
+ edges:
198
+ - match: "1, hours, schedule, time, hours"
199
+ goto: hours
200
+ - match: "2, catalog, product, brochure"
201
+ goto: catalog
202
+ - match: "3, human, agent, person, talk, speak"
203
+ goto: escalate
204
+ - match: "4, price, pricing, cost"
205
+ goto: pricing
206
+ - match: "0, exit, bye, goodbye, end"
207
+ goto: farewell
208
+ - goto: invalid # Default edge (no 'match'): catches everything else
209
+
210
+ hours:
211
+ action: hours
212
+ edges:
213
+ - match: "menu, back, return, volver"
214
+ goto: menu
215
+ - match: "0, exit"
216
+ goto: farewell
217
+ - goto: invalid
218
+
219
+ catalog:
220
+ action: catalog
221
+ edges:
222
+ - match: "menu, back"
223
+ goto: menu
224
+ - match: "0, exit"
225
+ goto: farewell
226
+ - goto: invalid
227
+
228
+ escalate:
229
+ action: escalate # This action has cooldown
230
+ edges:
231
+ - match: "menu, back"
232
+ goto: menu
233
+ - match: "0, exit"
234
+ goto: farewell
235
+ - goto: invalid
236
+
237
+ pricing:
238
+ action: pricing
239
+ edges:
240
+ - match: "menu, back"
241
+ goto: menu
242
+ - match: "0, exit"
243
+ goto: farewell
244
+ - match: "interested, buy, order, quote"
245
+ goto: lead
246
+ - goto: invalid
247
+
248
+ lead:
249
+ action: lead-notify
250
+ edges:
251
+ - match: "menu, back"
252
+ goto: menu
253
+ - goto: farewell
254
+
255
+ invalid:
256
+ action: invalid
257
+ edges:
258
+ - goto: menu # Always return to menu after invalid input
259
+
260
+ farewell:
261
+ action: farewell
262
+ edges: [] # No edges — session stays alive until timeout
263
+ ```
264
+
265
+ **Graph entry behavior:**
266
+
267
+ When a sender has no active session, the bot automatically:
268
+ 1. Creates a new session
269
+ 2. Enters the graph's `root` node
270
+ 3. Executes the root node's action
271
+ 4. Stores the root node in the visited history
272
+ 5. Attempts to resolve the user's original message using the normal node resolution algorithm
273
+
274
+ This means the user's first message is never discarded — it can match a root edge and transition to a different node right away.
275
+
276
+ - **`match`** — comma-separated phrases. Fuzzy-matched against the user's message with the configured threshold.
277
+ - **`fuzzy_threshold`** — controls strictness. `0.3` = strict, `0.6` = moderate (default), `0.9` = loose.
278
+ - **`timeout`** — seconds of inactivity before the session expires. Defaults to global `default_timeout`.
279
+ - **`fallback`** — where to redirect if no edge matches. Without one, mismatched messages are silently ignored.
280
+ - **`edges: []`** — a node with no edges does not destroy the session; the user can still navigate to any previously visited node from its own edges. Sessions only end via timeout.
281
+
282
+ ### Cooldowns
283
+
284
+ Set a `guards.cooldown.duration` (seconds) on any action to prevent the same sender from triggering it repeatedly. Provide an `on_blocked` pipeline that runs when the guard blocks the action.
285
+
286
+ ```yaml
287
+ actions:
288
+ escalate:
289
+ guards:
290
+ cooldown:
291
+ duration: 120
292
+ on_blocked:
293
+ - message:
294
+ body: "You already requested an agent. Please wait."
295
+ steps:
296
+ - message:
297
+ body: "Connecting you to an agent."
298
+ - request:
299
+ name: escalate-human
300
+ url: "https://api.example.com/support/escalate"
301
+ ```
302
+
303
+ Cooldowns are per-sender, per-action — different senders are tracked independently. The `on_blocked` pipeline can contain any step types (message, request, location).
304
+
305
+ ### Location Actions
306
+
307
+ Send a WhatsApp location pin to the user. A `location` step can be combined with other steps in the pipeline.
308
+
309
+ ```yaml
310
+ actions:
311
+ send-office:
312
+ steps:
313
+ - message:
314
+ body: "Here is our office."
315
+ - location:
316
+ latitude: 19.4326
317
+ longitude: -99.1332
318
+ name: "Main Office"
319
+ address: "Av. Reforma 123, CDMX"
320
+ ```
321
+
322
+ Required: `latitude` (-90 to 90), `longitude` (-180 to 180). Optional: `name`, `address`, `url`, `description`.
323
+
324
+ ### Bot Settings
325
+
326
+ A bot references exactly one graph.
327
+
328
+ ```yaml
329
+ bots:
330
+ support-bot:
331
+ graph: faq-support
332
+ settings:
333
+ queue_delay: 1500
334
+ ignore_groups: true
335
+ ignored_senders:
336
+ - "status@broadcast"
337
+ ```
338
+
339
+ See [`config.example.yml`](config.example.yml) for a full working configuration.
340
+
341
+ ## Service Management
342
+
343
+ Once installed and configured, manage your bot service:
344
+
345
+ ```bash
346
+ # Check service status
347
+ systemctl --user status botforje
348
+
349
+ # View logs in real-time
350
+ journalctl --user -u botforje -f
351
+
352
+ # Restart service
353
+ systemctl --user restart botforje
354
+
355
+ # Stop service
356
+ systemctl --user stop botforje
357
+
358
+ # Disable auto-start
359
+ systemctl --user disable botforje
360
+ ```
361
+
362
+ ## Troubleshooting
363
+
364
+ **Service not starting?**
365
+ - Check if `xvfb` is installed: `which xvfb`
366
+ - Verify config file: `cat ~/.config/botforje/config.yml`
367
+ - Check service logs: `journalctl --user -u botforje -n 50`
368
+
369
+ **QR code not showing?**
370
+ - Ensure no other WhatsApp sessions are active
371
+ - Restart the service: `systemctl --user restart botforje`
372
+
373
+ **Messages not responding?**
374
+ - Check bot status in logs
375
+ - Verify the bot's `graph` field references an existing graph
376
+ - Test with exact phrases first, then tune `fuzzy_threshold`
377
+
378
+ **Request not working?**
379
+ - Test your endpoint with tools like Postman
380
+ - Check logs for timeout/connection errors
381
+ - Verify request URL and headers
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveVars = resolveVars;
4
+ exports.resolveAction = resolveAction;
5
+ exports.executeAction = executeAction;
6
+ exports.getAction = getAction;
7
+ function resolveVars(text, context) {
8
+ return text
9
+ .replace(/\{\{\s*sender\s*\}\}/g, context.sender)
10
+ .replace(/\{\{\s*senderName\s*\}\}/g, context.senderName ?? context.sender)
11
+ .replace(/\{\{\s*message\s*\}\}/g, context.message)
12
+ .replace(/\{\{\s*bot\.id\s*\}\}/g, context.botId)
13
+ .replace(/\{\{\s*variables\.([a-zA-Z0-9_]+)\s*\}\}/g, (_, key) => {
14
+ const value = context.variables[key];
15
+ return value !== undefined ? String(value) : '';
16
+ });
17
+ }
18
+ function resolveAction(catalog, id) {
19
+ const action = catalog.get(id);
20
+ if (!action) {
21
+ throw new Error(`Action "${id}" not found in catalog`);
22
+ }
23
+ return action;
24
+ }
25
+ function executeAction(catalog, actionId, context) {
26
+ const action = resolveAction(catalog, actionId);
27
+ const result = {};
28
+ if (action.reply) {
29
+ result.reply = resolveVars(action.reply, context);
30
+ }
31
+ if (action.webhook) {
32
+ result.webhook = {
33
+ ...action.webhook,
34
+ url: resolveVars(action.webhook.url, context),
35
+ };
36
+ }
37
+ if (action.location) {
38
+ result.location = { ...action.location };
39
+ }
40
+ return result;
41
+ }
42
+ function getAction(catalog, actionId) {
43
+ return resolveAction(catalog, actionId);
44
+ }
@@ -0,0 +1,40 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveAction = resolveAction;
4
+ exports.mapActionCatalog = mapActionCatalog;
5
+ function resolveAction(catalog, id) {
6
+ const action = catalog.get(id);
7
+ if (!action) {
8
+ throw new Error(`Action "${id}" not found in catalog`);
9
+ }
10
+ return action;
11
+ }
12
+ function mapActionCatalog(config) {
13
+ const catalog = new Map();
14
+ for (const [id, actionConfig] of Object.entries(config)) {
15
+ catalog.set(id, mapAction(id, actionConfig));
16
+ }
17
+ return catalog;
18
+ }
19
+ function mapAction(id, config) {
20
+ if (!config.reply && !config.webhook) {
21
+ throw new Error(`Action "${id}" must define reply, webhook, or both`);
22
+ }
23
+ return {
24
+ id,
25
+ reply: config.reply,
26
+ webhook: config.webhook ? mapWebhookAction(config.webhook) : undefined,
27
+ cooldown: config.cooldown,
28
+ cooldownReply: config.cooldown_reply,
29
+ };
30
+ }
31
+ function mapWebhookAction(config) {
32
+ return {
33
+ name: config.name,
34
+ url: config.url,
35
+ method: config.method ?? 'POST',
36
+ headers: config.headers ?? {},
37
+ timeout: config.timeout ?? 5000,
38
+ retries: config.retry ?? 3,
39
+ };
40
+ }
@@ -0,0 +1,60 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CooldownService = void 0;
4
+ class CooldownService {
5
+ constructor() {
6
+ this.cooldowns = new Map();
7
+ }
8
+ isOnCooldown(sender, key, cooldownMs) {
9
+ if (cooldownMs <= 0)
10
+ return false;
11
+ const senderCooldowns = this.cooldowns.get(sender);
12
+ if (!senderCooldowns)
13
+ return false;
14
+ const lastTrigger = senderCooldowns.get(key);
15
+ if (!lastTrigger)
16
+ return false;
17
+ return Date.now() - lastTrigger < cooldownMs;
18
+ }
19
+ setCooldown(sender, key) {
20
+ let senderCooldowns = this.cooldowns.get(sender);
21
+ if (!senderCooldowns) {
22
+ senderCooldowns = new Map();
23
+ this.cooldowns.set(sender, senderCooldowns);
24
+ }
25
+ senderCooldowns.set(key, Date.now());
26
+ }
27
+ cleanupExpiredCooldowns() {
28
+ const now = Date.now();
29
+ const maxAge = 60 * 60 * 1000;
30
+ for (const [sender, keyMap] of this.cooldowns.entries()) {
31
+ for (const [key, timestamp] of keyMap.entries()) {
32
+ if (now - timestamp > maxAge) {
33
+ keyMap.delete(key);
34
+ }
35
+ }
36
+ if (keyMap.size === 0) {
37
+ this.cooldowns.delete(sender);
38
+ }
39
+ }
40
+ }
41
+ getCooldownStatus(sender, key) {
42
+ const senderCooldowns = this.cooldowns.get(sender);
43
+ const lastTrigger = senderCooldowns?.get(key);
44
+ if (!lastTrigger) {
45
+ return { isOnCooldown: false, remainingMs: 0 };
46
+ }
47
+ const now = Date.now();
48
+ const elapsed = now - lastTrigger;
49
+ const isOnCooldown = elapsed < 30000;
50
+ const remainingMs = Math.max(0, 30000 - elapsed);
51
+ return { isOnCooldown, remainingMs, lastTrigger };
52
+ }
53
+ clearAllCooldowns() {
54
+ this.cooldowns.clear();
55
+ }
56
+ clearSenderCooldowns(sender) {
57
+ this.cooldowns.delete(sender);
58
+ }
59
+ }
60
+ exports.CooldownService = CooldownService;
@@ -0,0 +1,23 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.executeAction = executeAction;
4
+ exports.getAction = getAction;
5
+ const catalog_1 = require("./catalog");
6
+ const template_1 = require("./template");
7
+ function executeAction(catalog, actionId, context) {
8
+ const action = (0, catalog_1.resolveAction)(catalog, actionId);
9
+ const result = {};
10
+ if (action.reply) {
11
+ result.reply = (0, template_1.resolveVars)(action.reply, context);
12
+ }
13
+ if (action.webhook) {
14
+ result.webhook = {
15
+ ...action.webhook,
16
+ url: (0, template_1.resolveVars)(action.webhook.url, context),
17
+ };
18
+ }
19
+ return result;
20
+ }
21
+ function getAction(catalog, actionId) {
22
+ return (0, catalog_1.resolveAction)(catalog, actionId);
23
+ }
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveVars = resolveVars;
4
+ function resolveVars(text, context) {
5
+ return text
6
+ .replace(/\{\{\s*sender\s*\}\}/g, context.sender)
7
+ .replace(/\{\{\s*message\s*\}\}/g, context.message)
8
+ .replace(/\{\{\s*bot\.id\s*\}\}/g, context.botId)
9
+ .replace(/\{\{\s*variables\.([a-zA-Z0-9_]+)\s*\}\}/g, (_, key) => {
10
+ const value = context.variables[key];
11
+ return value !== undefined ? String(value) : '';
12
+ });
13
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sendWebhookRequest = sendWebhookRequest;
4
+ const logger_1 = require("../helpers/logger");
5
+ async function sendWebhookRequest(call) {
6
+ const logger = (0, logger_1.getLogger)();
7
+ const maxRetries = call.retries || 1;
8
+ let lastError = null;
9
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
10
+ try {
11
+ const response = await fetch(call.url, {
12
+ method: call.method,
13
+ headers: {
14
+ 'Content-Type': 'application/json',
15
+ ...call.headers,
16
+ },
17
+ body: call.body !== undefined ? JSON.stringify(call.body) : undefined,
18
+ signal: AbortSignal.timeout(call.timeout),
19
+ });
20
+ if (!response.ok) {
21
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
22
+ }
23
+ logger.info(`Webhook request successful: ${call.url}`);
24
+ return;
25
+ }
26
+ catch (error) {
27
+ lastError = error instanceof Error ? error : new Error(String(error));
28
+ if (attempt < maxRetries) {
29
+ const backoffMs = Math.pow(2, attempt - 1) * 1000;
30
+ logger.warn(`Webhook request failed (attempt ${attempt}/${maxRetries}), retrying in ${backoffMs}ms:`, lastError.message);
31
+ await new Promise(resolve => setTimeout(resolve, backoffMs));
32
+ }
33
+ }
34
+ }
35
+ throw new Error(`Webhook request failed after ${maxRetries} attempts: ${lastError?.message}`);
36
+ }
@@ -0,0 +1,26 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveVars = resolveVars;
4
+ exports.resolveAction = resolveAction;
5
+ exports.getAction = getAction;
6
+ function resolveVars(text, context) {
7
+ return text
8
+ .replace(/\{\{\s*senderPhone\s*\}\}/g, context.senderPhone)
9
+ .replace(/\{\{\s*senderName\s*\}\}/g, context.senderName ?? context.senderPhone)
10
+ .replace(/\{\{\s*message\s*\}\}/g, context.message)
11
+ .replace(/\{\{\s*bot\.id\s*\}\}/g, context.botId)
12
+ .replace(/\{\{\s*variables\.([a-zA-Z0-9_]+)\s*\}\}/g, (_, key) => {
13
+ const value = context.variables[key];
14
+ return value !== undefined ? String(value) : '';
15
+ });
16
+ }
17
+ function resolveAction(catalog, id) {
18
+ const action = catalog.get(id);
19
+ if (!action) {
20
+ throw new Error(`Action "${id}" not found in catalog`);
21
+ }
22
+ return action;
23
+ }
24
+ function getAction(catalog, actionId) {
25
+ return resolveAction(catalog, actionId);
26
+ }