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
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
+
exports.validateId = validateId;
|
|
37
|
+
exports.validateTypingDelay = validateTypingDelay;
|
|
38
|
+
exports.validateQueueDelay = validateQueueDelay;
|
|
39
|
+
exports.validateConfig = validateConfig;
|
|
40
|
+
const fs = __importStar(require("fs/promises"));
|
|
41
|
+
const fsSync = __importStar(require("fs"));
|
|
42
|
+
const path = __importStar(require("path"));
|
|
43
|
+
const yaml = __importStar(require("js-yaml"));
|
|
44
|
+
const yaml_1 = require("./yaml");
|
|
45
|
+
function validateId(id, kind) {
|
|
46
|
+
if (!id || id.length < 3) {
|
|
47
|
+
throw new Error(`${kind} ID must be at least 3 characters long`);
|
|
48
|
+
}
|
|
49
|
+
if (!/^[a-z0-9-]+$/.test(id)) {
|
|
50
|
+
throw new Error(`${kind} ID can only contain lowercase letters, numbers and hyphens`);
|
|
51
|
+
}
|
|
52
|
+
if (/^[-]/.test(id)) {
|
|
53
|
+
throw new Error(`${kind} ID cannot start with a hyphen`);
|
|
54
|
+
}
|
|
55
|
+
if (/[-]$/.test(id)) {
|
|
56
|
+
throw new Error(`${kind} ID cannot end with a hyphen`);
|
|
57
|
+
}
|
|
58
|
+
if (/--/.test(id)) {
|
|
59
|
+
throw new Error(`${kind} ID cannot contain consecutive hyphens`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function validateTypingDelay(delay) {
|
|
63
|
+
if (delay < 0) {
|
|
64
|
+
throw new Error('Typing delay must be non-negative');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
function validateQueueDelay(delay) {
|
|
68
|
+
if (delay < 0) {
|
|
69
|
+
throw new Error('Queue delay must be non-negative');
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
class FileContext {
|
|
73
|
+
constructor(filepath, content) {
|
|
74
|
+
this.errors = [];
|
|
75
|
+
this.filepath = filepath;
|
|
76
|
+
this.content = content;
|
|
77
|
+
this.lines = content.split('\n');
|
|
78
|
+
}
|
|
79
|
+
add(message, field) {
|
|
80
|
+
const line = field ? this.findLine(field) : null;
|
|
81
|
+
this.errors.push({ file: this.filepath, line, message });
|
|
82
|
+
}
|
|
83
|
+
get validationErrors() {
|
|
84
|
+
return this.errors;
|
|
85
|
+
}
|
|
86
|
+
findLine(field) {
|
|
87
|
+
for (let i = 0; i < this.lines.length; i++) {
|
|
88
|
+
if (new RegExp(`^\\s*${escapeRegex(field)}:`).test(this.lines[i])) {
|
|
89
|
+
return i + 1;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
function escapeRegex(s) {
|
|
96
|
+
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
97
|
+
}
|
|
98
|
+
function isValidLogLevel(value) {
|
|
99
|
+
return typeof value === 'string' && ['info', 'debug', 'warn', 'error'].includes(value);
|
|
100
|
+
}
|
|
101
|
+
function isValidHttpMethod(value) {
|
|
102
|
+
return typeof value === 'string' && ['GET', 'POST', 'PUT', 'PATCH'].includes(value.toUpperCase());
|
|
103
|
+
}
|
|
104
|
+
async function readAndParseYaml(filepath, ctx) {
|
|
105
|
+
try {
|
|
106
|
+
const raw = await fs.readFile(filepath, 'utf-8');
|
|
107
|
+
ctx.content = raw;
|
|
108
|
+
ctx.lines = raw.split('\n');
|
|
109
|
+
const parsed = yaml.load(raw);
|
|
110
|
+
if (parsed === null || parsed === undefined) {
|
|
111
|
+
ctx.add('File is empty');
|
|
112
|
+
return null;
|
|
113
|
+
}
|
|
114
|
+
if (typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
115
|
+
ctx.add('File must contain a YAML object, not an array or scalar', undefined);
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
return parsed;
|
|
119
|
+
}
|
|
120
|
+
catch (e) {
|
|
121
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
122
|
+
ctx.add(`YAML parse error: ${msg}`);
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
function validateConfigFile(config, ctx) {
|
|
127
|
+
if (config.chromium_path !== undefined && typeof config.chromium_path !== 'string') {
|
|
128
|
+
ctx.add('chromium_path must be a string', 'chromium_path');
|
|
129
|
+
}
|
|
130
|
+
if (config.port !== undefined) {
|
|
131
|
+
if (typeof config.port !== 'number' || !Number.isInteger(config.port) || config.port < 0) {
|
|
132
|
+
ctx.add('port must be a non-negative integer', 'port');
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
if (config.trusted_issuers !== undefined) {
|
|
136
|
+
if (!Array.isArray(config.trusted_issuers) || !config.trusted_issuers.every(i => typeof i === 'string')) {
|
|
137
|
+
ctx.add('trusted_issuers must be an array of strings', 'trusted_issuers');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (config.address !== undefined) {
|
|
141
|
+
if (typeof config.address !== 'string' || config.address.length === 0) {
|
|
142
|
+
ctx.add('address must be a non-empty string', 'address');
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
if (config.log_level !== undefined && !isValidLogLevel(config.log_level)) {
|
|
146
|
+
ctx.add('log_level must be one of: info, debug, warn, error', 'log_level');
|
|
147
|
+
}
|
|
148
|
+
if (config.default_timeout !== undefined) {
|
|
149
|
+
if (typeof config.default_timeout !== 'number' || !Number.isInteger(config.default_timeout) || config.default_timeout < 0) {
|
|
150
|
+
ctx.add('default_timeout must be a non-negative integer', 'default_timeout');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
function validateActionFile(id, data, ctx) {
|
|
155
|
+
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
|
|
156
|
+
ctx.add('Action must be a YAML object');
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
const a = data;
|
|
160
|
+
if (a.steps !== undefined && !Array.isArray(a.steps)) {
|
|
161
|
+
ctx.add('action.steps must be an array', 'steps');
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
if (a.guards !== undefined) {
|
|
165
|
+
if (typeof a.guards !== 'object' || a.guards === null || Array.isArray(a.guards)) {
|
|
166
|
+
ctx.add('action.guards must be an object', 'guards');
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
validateGuards(a.guards, ctx);
|
|
170
|
+
}
|
|
171
|
+
const hasSteps = Array.isArray(a.steps) && a.steps.length > 0;
|
|
172
|
+
const cooldownGuard = a.guards && typeof a.guards === 'object'
|
|
173
|
+
? a.guards.cooldown : undefined;
|
|
174
|
+
const hasOnBlocked = cooldownGuard && typeof cooldownGuard === 'object'
|
|
175
|
+
? Array.isArray(cooldownGuard.on_blocked)
|
|
176
|
+
&& cooldownGuard.on_blocked.length > 0
|
|
177
|
+
: false;
|
|
178
|
+
if (!hasSteps && !hasOnBlocked) {
|
|
179
|
+
ctx.add('Action must define steps or a cooldown guard with on_blocked');
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
if (Array.isArray(a.steps)) {
|
|
183
|
+
a.steps.forEach((step, idx) => {
|
|
184
|
+
validateStep(step, ctx, `steps[${idx}]`);
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
if (hasOnBlocked) {
|
|
188
|
+
const onBlocked = cooldownGuard.on_blocked;
|
|
189
|
+
onBlocked.forEach((step, idx) => {
|
|
190
|
+
validateStep(step, ctx, `guards.cooldown.on_blocked[${idx}]`);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return data;
|
|
194
|
+
}
|
|
195
|
+
function validateGuards(guards, ctx) {
|
|
196
|
+
if (guards.cooldown !== undefined) {
|
|
197
|
+
if (typeof guards.cooldown !== 'object' || guards.cooldown === null || Array.isArray(guards.cooldown)) {
|
|
198
|
+
ctx.add('action.guards.cooldown must be an object', 'cooldown');
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const c = guards.cooldown;
|
|
202
|
+
if (typeof c.duration !== 'number' || c.duration < 0) {
|
|
203
|
+
ctx.add('action.guards.cooldown.duration must be a non-negative number', 'duration');
|
|
204
|
+
}
|
|
205
|
+
if (c.on_blocked !== undefined) {
|
|
206
|
+
if (!Array.isArray(c.on_blocked)) {
|
|
207
|
+
ctx.add('action.guards.cooldown.on_blocked must be an array', 'on_blocked');
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
function validateStep(step, ctx, path) {
|
|
213
|
+
if (typeof step !== 'object' || step === null || Array.isArray(step)) {
|
|
214
|
+
ctx.add(`action.${path} must be an object`, path);
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const s = step;
|
|
218
|
+
const keys = Object.keys(s);
|
|
219
|
+
const validKeys = ['message', 'request', 'location'];
|
|
220
|
+
const stepKeys = keys.filter(k => validKeys.includes(k));
|
|
221
|
+
if (stepKeys.length === 0) {
|
|
222
|
+
ctx.add(`action.${path} must have exactly one of: message, request, location`, path);
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
if (stepKeys.length > 1) {
|
|
226
|
+
ctx.add(`action.${path} must have exactly one of: message, request, location (found: ${stepKeys.join(', ')})`, path);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
if (s.message !== undefined) {
|
|
230
|
+
validateMessageStep(s.message, ctx, `${path}.message`);
|
|
231
|
+
}
|
|
232
|
+
if (s.request !== undefined) {
|
|
233
|
+
validateRequestStep(s.request, ctx, `${path}.request`);
|
|
234
|
+
}
|
|
235
|
+
if (s.location !== undefined) {
|
|
236
|
+
validateLocationStep(s.location, ctx, `${path}.location`);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
function validateMessageStep(msg, ctx, path) {
|
|
240
|
+
if (typeof msg !== 'object' || msg === null || Array.isArray(msg)) {
|
|
241
|
+
ctx.add(`action.${path} must be an object`, path);
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
const m = msg;
|
|
245
|
+
if (typeof m.body !== 'string' || !m.body) {
|
|
246
|
+
ctx.add(`action.${path}.body is required and must be a non-empty string`, `${path}.body`);
|
|
247
|
+
}
|
|
248
|
+
if (m.to !== undefined && typeof m.to !== 'string') {
|
|
249
|
+
ctx.add(`action.${path}.to must be a string`, `${path}.to`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function validateRequestStep(wh, ctx, path) {
|
|
253
|
+
if (typeof wh !== 'object' || wh === null || Array.isArray(wh)) {
|
|
254
|
+
ctx.add(`action.${path} must be an object`, path);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
const w = wh;
|
|
258
|
+
if (typeof w.url !== 'string') {
|
|
259
|
+
ctx.add(`action.${path}.url must be a string`, `${path}.url`);
|
|
260
|
+
}
|
|
261
|
+
if (w.method !== undefined && !isValidHttpMethod(w.method)) {
|
|
262
|
+
ctx.add(`action.${path}.method must be GET, POST, PUT, or PATCH`, `${path}.method`);
|
|
263
|
+
}
|
|
264
|
+
if (w.timeout !== undefined && (typeof w.timeout !== 'number' || w.timeout < 0)) {
|
|
265
|
+
ctx.add(`action.${path}.timeout must be a non-negative number`, `${path}.timeout`);
|
|
266
|
+
}
|
|
267
|
+
if (w.retry !== undefined && (typeof w.retry !== 'number' || w.retry < 0 || !Number.isInteger(w.retry))) {
|
|
268
|
+
ctx.add(`action.${path}.retry must be a non-negative integer`, `${path}.retry`);
|
|
269
|
+
}
|
|
270
|
+
if (w.headers !== undefined && (typeof w.headers !== 'object' || w.headers === null || Array.isArray(w.headers))) {
|
|
271
|
+
ctx.add(`action.${path}.headers must be an object`, `${path}.headers`);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
function validateLocationStep(loc, ctx, path) {
|
|
275
|
+
if (typeof loc !== 'object' || loc === null || Array.isArray(loc)) {
|
|
276
|
+
ctx.add(`action.${path} must be an object`, path);
|
|
277
|
+
return;
|
|
278
|
+
}
|
|
279
|
+
const l = loc;
|
|
280
|
+
if (typeof l.latitude !== 'number' || !Number.isFinite(l.latitude)) {
|
|
281
|
+
ctx.add(`action.${path}.latitude must be a number`, `${path}.latitude`);
|
|
282
|
+
}
|
|
283
|
+
else if (l.latitude < -90 || l.latitude > 90) {
|
|
284
|
+
ctx.add(`action.${path}.latitude must be between -90 and 90`, `${path}.latitude`);
|
|
285
|
+
}
|
|
286
|
+
if (typeof l.longitude !== 'number' || !Number.isFinite(l.longitude)) {
|
|
287
|
+
ctx.add(`action.${path}.longitude must be a number`, `${path}.longitude`);
|
|
288
|
+
}
|
|
289
|
+
else if (l.longitude < -180 || l.longitude > 180) {
|
|
290
|
+
ctx.add(`action.${path}.longitude must be between -180 and 180`, `${path}.longitude`);
|
|
291
|
+
}
|
|
292
|
+
if (l.name !== undefined && typeof l.name !== 'string') {
|
|
293
|
+
ctx.add(`action.${path}.name must be a string`, `${path}.name`);
|
|
294
|
+
}
|
|
295
|
+
if (l.address !== undefined && typeof l.address !== 'string') {
|
|
296
|
+
ctx.add(`action.${path}.address must be a string`, `${path}.address`);
|
|
297
|
+
}
|
|
298
|
+
if (l.url !== undefined && typeof l.url !== 'string') {
|
|
299
|
+
ctx.add(`action.${path}.url must be a string`, `${path}.url`);
|
|
300
|
+
}
|
|
301
|
+
if (l.description !== undefined && typeof l.description !== 'string') {
|
|
302
|
+
ctx.add(`action.${path}.description must be a string`, `${path}.description`);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
function validateGraphFile(id, data, ctx, allNodes) {
|
|
306
|
+
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
|
|
307
|
+
ctx.add('Graph must be a YAML object');
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
const g = data;
|
|
311
|
+
if (typeof g.root !== 'string' || !g.root) {
|
|
312
|
+
ctx.add('graph.root is required and must be a non-empty string', 'root');
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
if (!g.nodes || typeof g.nodes !== 'object' || Array.isArray(g.nodes)) {
|
|
316
|
+
ctx.add('graph.nodes is required and must be an object', 'nodes');
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
const nodes = g.nodes;
|
|
320
|
+
const nodeKeys = Object.keys(nodes);
|
|
321
|
+
if (nodeKeys.length === 0) {
|
|
322
|
+
ctx.add('graph.nodes must have at least one node', 'nodes');
|
|
323
|
+
return null;
|
|
324
|
+
}
|
|
325
|
+
nodeKeys.forEach(nk => allNodes.add(nk));
|
|
326
|
+
if (!nodes[g.root]) {
|
|
327
|
+
ctx.add(`graph.root "${g.root}" not found in nodes`, 'root');
|
|
328
|
+
}
|
|
329
|
+
if (g.fallback !== undefined) {
|
|
330
|
+
if (typeof g.fallback !== 'string') {
|
|
331
|
+
ctx.add('graph.fallback must be a string', 'fallback');
|
|
332
|
+
}
|
|
333
|
+
else if (!nodes[g.fallback]) {
|
|
334
|
+
ctx.add(`graph.fallback "${g.fallback}" not found in nodes`, 'fallback');
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
if (g.timeout !== undefined) {
|
|
338
|
+
if (typeof g.timeout !== 'number' || g.timeout < 0) {
|
|
339
|
+
ctx.add('graph.timeout must be a non-negative number', 'timeout');
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
if (g.triggers !== undefined) {
|
|
343
|
+
ctx.add('graphs no longer support triggers; remove the triggers field; the bot auto-enters the graph root on first message', 'triggers');
|
|
344
|
+
}
|
|
345
|
+
for (const [nodeId, nodeData] of Object.entries(nodes)) {
|
|
346
|
+
if (typeof nodeData !== 'object' || nodeData === null) {
|
|
347
|
+
ctx.add(`graph.nodes.${nodeId} must be an object`, nodeId);
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
const node = nodeData;
|
|
351
|
+
if (typeof node.action !== 'string' || !node.action) {
|
|
352
|
+
ctx.add(`graph.nodes.${nodeId}.action is required and must be a non-empty string`, nodeId);
|
|
353
|
+
}
|
|
354
|
+
if (node.edges !== undefined) {
|
|
355
|
+
if (!Array.isArray(node.edges)) {
|
|
356
|
+
ctx.add(`graph.nodes.${nodeId}.edges must be an array`, nodeId);
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
for (let ei = 0; ei < node.edges.length; ei++) {
|
|
360
|
+
const edge = node.edges[ei];
|
|
361
|
+
if (typeof edge !== 'object' || edge === null) {
|
|
362
|
+
ctx.add(`graph.nodes.${nodeId}.edges[${ei}] must be an object`, nodeId);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
const e = edge;
|
|
366
|
+
if (e.match !== undefined) {
|
|
367
|
+
if (typeof e.match !== 'string' && !Array.isArray(e.match)) {
|
|
368
|
+
ctx.add(`graph.nodes.${nodeId}.edges[${ei}].match must be a string or array`, nodeId);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
if (e.fuzzy_threshold !== undefined && (typeof e.fuzzy_threshold !== 'number' || e.fuzzy_threshold < 0 || e.fuzzy_threshold > 1)) {
|
|
372
|
+
ctx.add(`graph.nodes.${nodeId}.edges[${ei}].fuzzy_threshold must be between 0 and 1`, nodeId);
|
|
373
|
+
}
|
|
374
|
+
if (typeof e.goto !== 'string' || !e.goto) {
|
|
375
|
+
ctx.add(`graph.nodes.${nodeId}.edges[${ei}].goto is required and must be a non-empty string`, nodeId);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
return data;
|
|
382
|
+
}
|
|
383
|
+
function findUnreachableNodes(graph) {
|
|
384
|
+
const reachable = new Set();
|
|
385
|
+
const queue = [graph.root];
|
|
386
|
+
reachable.add(graph.root);
|
|
387
|
+
while (queue.length > 0) {
|
|
388
|
+
const current = queue.shift();
|
|
389
|
+
const node = graph.nodes[current];
|
|
390
|
+
if (!node || !node.edges)
|
|
391
|
+
continue;
|
|
392
|
+
for (const edge of node.edges) {
|
|
393
|
+
if (edge.goto && !reachable.has(edge.goto) && graph.nodes[edge.goto]) {
|
|
394
|
+
reachable.add(edge.goto);
|
|
395
|
+
queue.push(edge.goto);
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
return Object.keys(graph.nodes).filter(n => !reachable.has(n));
|
|
400
|
+
}
|
|
401
|
+
function validateBotFile(id, data, ctx) {
|
|
402
|
+
if (typeof data !== 'object' || data === null || Array.isArray(data)) {
|
|
403
|
+
ctx.add('Bot must be a YAML object');
|
|
404
|
+
return null;
|
|
405
|
+
}
|
|
406
|
+
const b = data;
|
|
407
|
+
if (b.graph !== undefined) {
|
|
408
|
+
if (typeof b.graph !== 'string' || !b.graph) {
|
|
409
|
+
ctx.add('bot.graph must be a non-empty string', 'graph');
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
if (b.settings !== undefined) {
|
|
413
|
+
if (typeof b.settings !== 'object' || b.settings === null || Array.isArray(b.settings)) {
|
|
414
|
+
ctx.add('bot.settings must be an object', 'settings');
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
const s = b.settings;
|
|
418
|
+
if (s.simulate_typing !== undefined && typeof s.simulate_typing !== 'boolean') {
|
|
419
|
+
ctx.add('bot.settings.simulate_typing must be a boolean', 'simulate_typing');
|
|
420
|
+
}
|
|
421
|
+
if (s.typing_delay !== undefined && (typeof s.typing_delay !== 'number' || s.typing_delay < 0)) {
|
|
422
|
+
ctx.add('bot.settings.typing_delay must be a non-negative number', 'typing_delay');
|
|
423
|
+
}
|
|
424
|
+
if (s.queue_delay !== undefined && (typeof s.queue_delay !== 'number' || s.queue_delay < 0)) {
|
|
425
|
+
ctx.add('bot.settings.queue_delay must be a non-negative number', 'queue_delay');
|
|
426
|
+
}
|
|
427
|
+
if (s.read_receipts !== undefined && typeof s.read_receipts !== 'boolean') {
|
|
428
|
+
ctx.add('bot.settings.read_receipts must be a boolean', 'read_receipts');
|
|
429
|
+
}
|
|
430
|
+
if (s.ignore_groups !== undefined && typeof s.ignore_groups !== 'boolean') {
|
|
431
|
+
ctx.add('bot.settings.ignore_groups must be a boolean', 'ignore_groups');
|
|
432
|
+
}
|
|
433
|
+
if (s.allowed_senders !== undefined) {
|
|
434
|
+
if (!Array.isArray(s.allowed_senders)) {
|
|
435
|
+
ctx.add('bot.settings.allowed_senders must be an array', 'allowed_senders');
|
|
436
|
+
}
|
|
437
|
+
else {
|
|
438
|
+
for (let si = 0; si < s.allowed_senders.length; si++) {
|
|
439
|
+
if (typeof s.allowed_senders[si] !== 'string') {
|
|
440
|
+
ctx.add(`bot.settings.allowed_senders[${si}] must be a string`, 'allowed_senders');
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
if (s.ignored_senders !== undefined) {
|
|
446
|
+
if (!Array.isArray(s.ignored_senders)) {
|
|
447
|
+
ctx.add('bot.settings.ignored_senders must be an array', 'ignored_senders');
|
|
448
|
+
}
|
|
449
|
+
else {
|
|
450
|
+
for (let si = 0; si < s.ignored_senders.length; si++) {
|
|
451
|
+
if (typeof s.ignored_senders[si] !== 'string') {
|
|
452
|
+
ctx.add(`bot.settings.ignored_senders[${si}] must be a string`, 'ignored_senders');
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
return data;
|
|
460
|
+
}
|
|
461
|
+
function validateIdWithContext(id, kind, ctx) {
|
|
462
|
+
try {
|
|
463
|
+
validateId(id, kind);
|
|
464
|
+
}
|
|
465
|
+
catch (e) {
|
|
466
|
+
ctx.add(e.message);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
async function validateConfig(configPath) {
|
|
470
|
+
const targetPath = configPath || (0, yaml_1.getDefaultConfigPath)();
|
|
471
|
+
const configDir = path.dirname(targetPath);
|
|
472
|
+
const errors = [];
|
|
473
|
+
const actionsDir = path.join(configDir, 'actions');
|
|
474
|
+
const graphsDir = path.join(configDir, 'graphs');
|
|
475
|
+
const botsDir = path.join(configDir, 'bots');
|
|
476
|
+
const allInlineActions = new Map();
|
|
477
|
+
const allDirActions = new Map();
|
|
478
|
+
const allInlineGraphs = new Map();
|
|
479
|
+
const allDirGraphs = new Map();
|
|
480
|
+
const allInlineBots = new Map();
|
|
481
|
+
const allDirBots = new Map();
|
|
482
|
+
if (!fsSync.existsSync(targetPath)) {
|
|
483
|
+
errors.push({ file: targetPath, line: null, message: `Config file not found at ${targetPath}` });
|
|
484
|
+
return { errors, valid: false };
|
|
485
|
+
}
|
|
486
|
+
const configRaw = await fs.readFile(targetPath, 'utf-8');
|
|
487
|
+
const configCtx = new FileContext(targetPath, configRaw);
|
|
488
|
+
let configParsed = null;
|
|
489
|
+
try {
|
|
490
|
+
const parsed = yaml.load(configRaw);
|
|
491
|
+
if (parsed === null || parsed === undefined) {
|
|
492
|
+
configCtx.add('Config file is empty');
|
|
493
|
+
}
|
|
494
|
+
else if (typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
495
|
+
configCtx.add('Config must contain a YAML object at the root');
|
|
496
|
+
}
|
|
497
|
+
else {
|
|
498
|
+
configParsed = parsed;
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
catch (e) {
|
|
502
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
503
|
+
configCtx.add(`YAML parse error: ${msg}`);
|
|
504
|
+
}
|
|
505
|
+
if (configParsed && typeof configParsed === 'object' && !Array.isArray(configParsed)) {
|
|
506
|
+
const configObj = configParsed;
|
|
507
|
+
validateConfigFile(configObj, configCtx);
|
|
508
|
+
if (configObj.bots !== undefined) {
|
|
509
|
+
if (typeof configObj.bots !== 'object' || configObj.bots === null || Array.isArray(configObj.bots)) {
|
|
510
|
+
configCtx.add('bots must be a YAML object (map of bot IDs to bot configs)', 'bots');
|
|
511
|
+
}
|
|
512
|
+
else {
|
|
513
|
+
for (const [botId, botData] of Object.entries(configObj.bots)) {
|
|
514
|
+
validateIdWithContext(botId, 'Bot', configCtx);
|
|
515
|
+
const botCtx = new FileContext(targetPath, configRaw);
|
|
516
|
+
const botConfig = validateBotFile(botId, botData, botCtx);
|
|
517
|
+
if (botConfig)
|
|
518
|
+
allInlineBots.set(botId, botConfig);
|
|
519
|
+
botCtx.validationErrors.forEach(e => {
|
|
520
|
+
e.message = `bots.${botId}: ${e.message}`;
|
|
521
|
+
errors.push(e);
|
|
522
|
+
});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
if (configObj.actions !== undefined) {
|
|
527
|
+
if (typeof configObj.actions !== 'object' || configObj.actions === null || Array.isArray(configObj.actions)) {
|
|
528
|
+
configCtx.add('actions must be a YAML object (map of action IDs to action configs)', 'actions');
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
for (const [actionId, actionData] of Object.entries(configObj.actions)) {
|
|
532
|
+
validateIdWithContext(actionId, 'Action', configCtx);
|
|
533
|
+
const actionCtx = new FileContext(targetPath, configRaw);
|
|
534
|
+
const actionConfig = validateActionFile(actionId, actionData, actionCtx);
|
|
535
|
+
if (actionConfig)
|
|
536
|
+
allInlineActions.set(actionId, actionConfig);
|
|
537
|
+
actionCtx.validationErrors.forEach(e => {
|
|
538
|
+
e.message = `actions.${actionId}: ${e.message}`;
|
|
539
|
+
errors.push(e);
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
if (configObj.graphs !== undefined) {
|
|
545
|
+
if (typeof configObj.graphs !== 'object' || configObj.graphs === null || Array.isArray(configObj.graphs)) {
|
|
546
|
+
configCtx.add('graphs must be a YAML object (map of graph IDs to graph configs)', 'graphs');
|
|
547
|
+
}
|
|
548
|
+
else {
|
|
549
|
+
for (const [graphId, graphData] of Object.entries(configObj.graphs)) {
|
|
550
|
+
validateIdWithContext(graphId, 'Graph', configCtx);
|
|
551
|
+
const allNodes = new Set();
|
|
552
|
+
const graphCtx = new FileContext(targetPath, configRaw);
|
|
553
|
+
const graphConfig = validateGraphFile(graphId, graphData, graphCtx, allNodes);
|
|
554
|
+
if (graphConfig) {
|
|
555
|
+
allInlineGraphs.set(graphId, graphConfig);
|
|
556
|
+
}
|
|
557
|
+
graphCtx.validationErrors.forEach(e => {
|
|
558
|
+
e.message = `graphs.${graphId}: ${e.message}`;
|
|
559
|
+
errors.push(e);
|
|
560
|
+
});
|
|
561
|
+
if (graphConfig && allNodes.size > 0) {
|
|
562
|
+
const edgeErrors = validateGraphEdgeTargets(graphId, graphConfig, allNodes, targetPath, configRaw);
|
|
563
|
+
edgeErrors.forEach(e => errors.push(e));
|
|
564
|
+
const unreachable = findUnreachableNodes(graphConfig);
|
|
565
|
+
for (const nodeName of unreachable) {
|
|
566
|
+
errors.push({
|
|
567
|
+
file: targetPath,
|
|
568
|
+
line: null,
|
|
569
|
+
message: `graphs.${graphId}: warning: node "${nodeName}" is unreachable from root "${graphConfig.root}"`,
|
|
570
|
+
});
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
errors.push(...configCtx.validationErrors);
|
|
578
|
+
if (fsSync.existsSync(actionsDir)) {
|
|
579
|
+
const actionFiles = await loadDirFiles(actionsDir);
|
|
580
|
+
for (const file of actionFiles) {
|
|
581
|
+
const actionId = path.basename(file, path.extname(file));
|
|
582
|
+
validateIdWithContext(actionId, 'Action', new FileContext(actionsDir, ''));
|
|
583
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
584
|
+
const ctx = new FileContext(file, content);
|
|
585
|
+
const parsed = await readAndParseYaml(file, ctx);
|
|
586
|
+
if (parsed) {
|
|
587
|
+
const actionConfig = validateActionFile(actionId, parsed, ctx);
|
|
588
|
+
if (actionConfig)
|
|
589
|
+
allDirActions.set(actionId, actionConfig);
|
|
590
|
+
}
|
|
591
|
+
errors.push(...ctx.validationErrors);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
if (fsSync.existsSync(graphsDir)) {
|
|
595
|
+
const graphFiles = await loadDirFiles(graphsDir);
|
|
596
|
+
for (const file of graphFiles) {
|
|
597
|
+
const graphId = path.basename(file, path.extname(file));
|
|
598
|
+
validateIdWithContext(graphId, 'Graph', new FileContext(graphsDir, ''));
|
|
599
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
600
|
+
const ctx = new FileContext(file, content);
|
|
601
|
+
const allNodes = new Set();
|
|
602
|
+
const parsed = await readAndParseYaml(file, ctx);
|
|
603
|
+
if (parsed) {
|
|
604
|
+
const graphConfig = validateGraphFile(graphId, parsed, ctx, allNodes);
|
|
605
|
+
if (graphConfig) {
|
|
606
|
+
allDirGraphs.set(graphId, graphConfig);
|
|
607
|
+
}
|
|
608
|
+
if (graphConfig && allNodes.size > 0) {
|
|
609
|
+
const edgeErrors = validateGraphEdgeTargets(graphId, graphConfig, allNodes, file, content);
|
|
610
|
+
edgeErrors.forEach(e => errors.push(e));
|
|
611
|
+
const unreachable = findUnreachableNodes(graphConfig);
|
|
612
|
+
for (const nodeName of unreachable) {
|
|
613
|
+
errors.push({
|
|
614
|
+
file: file,
|
|
615
|
+
line: null,
|
|
616
|
+
message: `graphs.${graphId}: warning: node "${nodeName}" is unreachable from root "${graphConfig.root}"`,
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
errors.push(...ctx.validationErrors);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
if (fsSync.existsSync(botsDir)) {
|
|
625
|
+
const botFiles = await loadDirFiles(botsDir);
|
|
626
|
+
for (const file of botFiles) {
|
|
627
|
+
const botId = path.basename(file, path.extname(file));
|
|
628
|
+
validateIdWithContext(botId, 'Bot', new FileContext(botsDir, ''));
|
|
629
|
+
const content = await fs.readFile(file, 'utf-8');
|
|
630
|
+
const ctx = new FileContext(file, content);
|
|
631
|
+
const parsed = await readAndParseYaml(file, ctx);
|
|
632
|
+
if (parsed) {
|
|
633
|
+
const botConfig = validateBotFile(botId, parsed, ctx);
|
|
634
|
+
if (botConfig)
|
|
635
|
+
allDirBots.set(botId, botConfig);
|
|
636
|
+
}
|
|
637
|
+
errors.push(...ctx.validationErrors);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
const mergedActions = new Map([...allDirActions, ...allInlineActions]);
|
|
641
|
+
const mergedGraphs = new Map([...allDirGraphs, ...allInlineGraphs]);
|
|
642
|
+
const mergedBots = new Map([...allDirBots, ...allInlineBots]);
|
|
643
|
+
if (mergedBots.size === 0) {
|
|
644
|
+
errors.push({ file: targetPath, line: null, message: 'No bots defined. At least one bot is required.' });
|
|
645
|
+
}
|
|
646
|
+
for (const [graphId, graph] of mergedGraphs) {
|
|
647
|
+
for (const [nodeId, node] of Object.entries(graph.nodes)) {
|
|
648
|
+
if (node.action && !mergedActions.has(node.action)) {
|
|
649
|
+
errors.push({
|
|
650
|
+
file: targetPath,
|
|
651
|
+
line: null,
|
|
652
|
+
message: `Graph "${graphId}" node "${nodeId}" references action "${node.action}" which is not defined`,
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
for (const [botId, bot] of mergedBots) {
|
|
658
|
+
if (bot.graph && !mergedGraphs.has(bot.graph)) {
|
|
659
|
+
errors.push({
|
|
660
|
+
file: targetPath,
|
|
661
|
+
line: null,
|
|
662
|
+
message: `Bot "${botId}" references graph "${bot.graph}" which is not defined`,
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
if (mergedBots.size === 0 && fsSync.existsSync(botsDir)) {
|
|
667
|
+
const botFiles = await fs.readdir(botsDir);
|
|
668
|
+
const yamlBots = botFiles.filter(f => f.endsWith('.yml') || f.endsWith('.yaml'));
|
|
669
|
+
if (yamlBots.length > 0) {
|
|
670
|
+
errors.push({
|
|
671
|
+
file: targetPath,
|
|
672
|
+
line: null,
|
|
673
|
+
message: 'Bot files found in bots/ but none were valid',
|
|
674
|
+
});
|
|
675
|
+
}
|
|
676
|
+
}
|
|
677
|
+
return {
|
|
678
|
+
errors,
|
|
679
|
+
valid: errors.length === 0
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
function validateGraphEdgeTargets(graphId, graph, allNodes, sourceFile, sourceContent) {
|
|
683
|
+
const edgeErrors = [];
|
|
684
|
+
for (const [nodeId, node] of Object.entries(graph.nodes)) {
|
|
685
|
+
if (node.edges) {
|
|
686
|
+
for (let ei = 0; ei < node.edges.length; ei++) {
|
|
687
|
+
const edge = node.edges[ei];
|
|
688
|
+
if (edge.goto && !allNodes.has(edge.goto)) {
|
|
689
|
+
edgeErrors.push({
|
|
690
|
+
file: sourceFile,
|
|
691
|
+
line: null,
|
|
692
|
+
message: `Graph "${graphId}" node "${nodeId}" edges[${ei}] goto "${edge.goto}" not found in nodes`,
|
|
693
|
+
});
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
return edgeErrors;
|
|
699
|
+
}
|
|
700
|
+
async function loadDirFiles(dir) {
|
|
701
|
+
const entries = await fs.readdir(dir);
|
|
702
|
+
return entries
|
|
703
|
+
.filter(f => f.endsWith('.yml') || f.endsWith('.yaml'))
|
|
704
|
+
.map(f => path.join(dir, f));
|
|
705
|
+
}
|