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.
- package/LICENSE +21 -0
- package/README.md +381 -0
- package/dist/action/action.js +44 -0
- package/dist/action/catalog.js +40 -0
- package/dist/action/cooldown.js +60 -0
- package/dist/action/executor.js +23 -0
- package/dist/action/template.js +13 -0
- package/dist/action/types.js +2 -0
- package/dist/action/webhook.js +36 -0
- package/dist/actions/action.js +26 -0
- package/dist/actions/cooldown.js +66 -0
- package/dist/actions/request.js +40 -0
- package/dist/api/routes/auth.js +56 -0
- package/dist/api/routes/bots.js +51 -0
- package/dist/api/routes/config.js +24 -0
- package/dist/api/routes/health.js +15 -0
- package/dist/api/routes/index.js +14 -0
- package/dist/api/routes/messages.js +69 -0
- package/dist/api/routes/sessions.js +97 -0
- package/dist/api/routes/status.js +38 -0
- package/dist/api/server.js +153 -0
- package/dist/auth/service.js +102 -0
- package/dist/boot/fleet.js +207 -0
- package/dist/bot/bot.js +30 -0
- package/dist/bot/fleet.js +211 -0
- package/dist/bot/fuzzy.js +20 -0
- package/dist/bot/mapper.js +47 -0
- package/dist/bot/types.js +2 -0
- package/dist/bot/validation.js +38 -0
- package/dist/bot.js +31 -0
- package/dist/cli/create-bot.js +84 -0
- package/dist/cli.js +138 -0
- package/dist/commands/auth.js +200 -0
- package/dist/commands/create-bot.js +64 -0
- package/dist/commands/guide.js +563 -0
- package/dist/commands/lock.js +73 -0
- package/dist/commands/status.js +145 -0
- package/dist/commands/unlock.js +88 -0
- package/dist/commands/validate.js +19 -0
- package/dist/config/mapper.js +152 -0
- package/dist/config/schema.js +2 -0
- package/dist/config/validation.js +705 -0
- package/dist/config/watcher.js +145 -0
- package/dist/config/yaml.js +197 -0
- package/dist/fleet.js +247 -0
- package/dist/flow/executor.js +286 -0
- package/dist/flow/flow.js +2 -0
- package/dist/flow/mapper.js +72 -0
- package/dist/flow/state.js +115 -0
- package/dist/flow/types.js +2 -0
- package/dist/graph/executor.js +294 -0
- package/dist/graph/graph.js +2 -0
- package/dist/graph/state.js +118 -0
- package/dist/helpers/data.js +42 -0
- package/dist/helpers/fuzzy.js +30 -0
- package/dist/helpers/logger.js +89 -0
- package/dist/helpers/validation.js +38 -0
- package/dist/index.js +92 -0
- package/dist/messages/contracts.js +2 -0
- package/dist/messages/inbox.js +58 -0
- package/dist/messages/outbox.js +136 -0
- package/dist/services/auto-response.js +62 -0
- package/dist/services/cooldown.js +60 -0
- package/dist/services/fleet.js +207 -0
- package/dist/services/flow-executor.js +195 -0
- package/dist/services/flow-state.js +118 -0
- package/dist/services/inbox.js +50 -0
- package/dist/services/message-handler.js +78 -0
- package/dist/services/message-queue.js +138 -0
- package/dist/services/outbox.js +138 -0
- package/dist/services/session.js +118 -0
- package/dist/services/webhook.js +87 -0
- package/dist/utils/logger.js +89 -0
- package/dist/utils/webhook.js +36 -0
- package/dist/validation/validate.js +592 -0
- package/dist/whatsapp/client.js +293 -0
- package/dist/whatsapp/session.js +158 -0
- package/dist/whatsapp/types.js +109 -0
- package/dist/whatsapp/whatsapp.js +109 -0
- package/package.json +97 -0
- package/scripts/postinstall.js +45 -0
- package/scripts/preuninstall.js +48 -0
- package/scripts/setup-systemd.js +91 -0
- 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
|
+
[](https://www.npmjs.com/package/botforje)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
[](https://nodejs.org)
|
|
6
|
+
[](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,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
|
+
}
|