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,102 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AuthService = void 0;
|
|
4
|
+
const node_sqlite_1 = require("node:sqlite");
|
|
5
|
+
const node_crypto_1 = require("node:crypto");
|
|
6
|
+
class AuthService {
|
|
7
|
+
constructor(dbPath) {
|
|
8
|
+
this.db = new node_sqlite_1.DatabaseSync(dbPath);
|
|
9
|
+
this.setupSchema();
|
|
10
|
+
}
|
|
11
|
+
setupSchema() {
|
|
12
|
+
this.db.exec(`
|
|
13
|
+
CREATE TABLE IF NOT EXISTS key_hash (
|
|
14
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
15
|
+
hash TEXT NOT NULL
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
19
|
+
id TEXT PRIMARY KEY,
|
|
20
|
+
token TEXT NOT NULL UNIQUE,
|
|
21
|
+
scope_bot_ids TEXT,
|
|
22
|
+
created_at INTEGER NOT NULL,
|
|
23
|
+
expires_at INTEGER NOT NULL
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_token ON sessions(token);
|
|
27
|
+
CREATE INDEX IF NOT EXISTS idx_sessions_expires ON sessions(expires_at);
|
|
28
|
+
`);
|
|
29
|
+
}
|
|
30
|
+
isLocked() {
|
|
31
|
+
const row = this.db.prepare('SELECT 1 FROM key_hash WHERE id = 1').get();
|
|
32
|
+
return !!row;
|
|
33
|
+
}
|
|
34
|
+
lock(key) {
|
|
35
|
+
if (this.isLocked()) {
|
|
36
|
+
throw new Error('Botforje is already locked. Run `botforje unlock` first.');
|
|
37
|
+
}
|
|
38
|
+
const secret = key || (0, node_crypto_1.randomBytes)(32).toString('hex');
|
|
39
|
+
if (secret.length < 32) {
|
|
40
|
+
throw new Error('Key must be at least 32 characters long');
|
|
41
|
+
}
|
|
42
|
+
const hash = (0, node_crypto_1.createHash)('sha256').update(secret).digest('hex');
|
|
43
|
+
this.db.prepare('INSERT OR REPLACE INTO key_hash (id, hash) VALUES (1, ?)').run(hash);
|
|
44
|
+
this.invalidateAllSessions();
|
|
45
|
+
return secret;
|
|
46
|
+
}
|
|
47
|
+
unlock(providedKey) {
|
|
48
|
+
const stored = this.db.prepare('SELECT hash FROM key_hash WHERE id = 1').get();
|
|
49
|
+
if (!stored)
|
|
50
|
+
return false;
|
|
51
|
+
const inputHash = (0, node_crypto_1.createHash)('sha256').update(providedKey).digest('hex');
|
|
52
|
+
const match = (0, node_crypto_1.timingSafeEqual)(Buffer.from(inputHash), Buffer.from(stored.hash));
|
|
53
|
+
if (match) {
|
|
54
|
+
this.db.prepare('DELETE FROM key_hash WHERE id = 1').run();
|
|
55
|
+
this.invalidateAllSessions();
|
|
56
|
+
}
|
|
57
|
+
return match;
|
|
58
|
+
}
|
|
59
|
+
verifyKey(key) {
|
|
60
|
+
const stored = this.db.prepare('SELECT hash FROM key_hash WHERE id = 1').get();
|
|
61
|
+
if (!stored)
|
|
62
|
+
return false;
|
|
63
|
+
const inputHash = (0, node_crypto_1.createHash)('sha256').update(key).digest('hex');
|
|
64
|
+
return (0, node_crypto_1.timingSafeEqual)(Buffer.from(inputHash), Buffer.from(stored.hash));
|
|
65
|
+
}
|
|
66
|
+
createSession(scopeBotIds) {
|
|
67
|
+
const id = (0, node_crypto_1.randomBytes)(16).toString('hex');
|
|
68
|
+
const token = (0, node_crypto_1.randomBytes)(32).toString('hex');
|
|
69
|
+
const now = Math.floor(Date.now() / 1000);
|
|
70
|
+
const expiresAt = now + 86400;
|
|
71
|
+
const scopeJson = scopeBotIds ? JSON.stringify(scopeBotIds) : null;
|
|
72
|
+
this.db.prepare('INSERT INTO sessions (id, token, scope_bot_ids, created_at, expires_at) VALUES (?, ?, ?, ?, ?)').run(id, token, scopeJson, now, expiresAt);
|
|
73
|
+
return { token, expiresAt };
|
|
74
|
+
}
|
|
75
|
+
validateSession(token) {
|
|
76
|
+
const row = this.db.prepare('SELECT * FROM sessions WHERE token = ?').get(token);
|
|
77
|
+
if (!row)
|
|
78
|
+
return null;
|
|
79
|
+
const now = Math.floor(Date.now() / 1000);
|
|
80
|
+
if (now > row.expires_at) {
|
|
81
|
+
this.db.prepare('DELETE FROM sessions WHERE id = ?').run(row.id);
|
|
82
|
+
return null;
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
id: row.id,
|
|
86
|
+
token: row.token,
|
|
87
|
+
scopeBotIds: row.scope_bot_ids ? JSON.parse(row.scope_bot_ids) : null,
|
|
88
|
+
createdAt: row.created_at,
|
|
89
|
+
expiresAt: row.expires_at,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
invalidateSession(token) {
|
|
93
|
+
this.db.prepare('DELETE FROM sessions WHERE token = ?').run(token);
|
|
94
|
+
}
|
|
95
|
+
invalidateAllSessions() {
|
|
96
|
+
this.db.prepare('DELETE FROM sessions').run();
|
|
97
|
+
}
|
|
98
|
+
close() {
|
|
99
|
+
this.db.close();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
exports.AuthService = AuthService;
|
|
@@ -0,0 +1,207 @@
|
|
|
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.BotFleet = void 0;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const mapper_1 = require("../bot/mapper");
|
|
39
|
+
const catalog_1 = require("../action/catalog");
|
|
40
|
+
const mapper_2 = require("../flow/mapper");
|
|
41
|
+
const cooldown_1 = require("../action/cooldown");
|
|
42
|
+
const inbox_1 = require("../services/inbox");
|
|
43
|
+
const state_1 = require("../flow/state");
|
|
44
|
+
const executor_1 = require("../flow/executor");
|
|
45
|
+
const session_1 = require("../whatsapp/session");
|
|
46
|
+
const yaml_1 = require("../config/yaml");
|
|
47
|
+
const logger_1 = require("../utils/logger");
|
|
48
|
+
class BotFleet {
|
|
49
|
+
constructor(outboxService) {
|
|
50
|
+
this.bots = new Map();
|
|
51
|
+
this.actionCatalog = new Map();
|
|
52
|
+
this.flowCatalog = new Map();
|
|
53
|
+
this.isRunning = false;
|
|
54
|
+
this.sessionManager = session_1.SessionManager.getInstance();
|
|
55
|
+
this.outboxService = outboxService;
|
|
56
|
+
this.cooldownService = new cooldown_1.CooldownService();
|
|
57
|
+
}
|
|
58
|
+
get logger() {
|
|
59
|
+
return (0, logger_1.getLogger)();
|
|
60
|
+
}
|
|
61
|
+
async start(configFile) {
|
|
62
|
+
if (this.isRunning) {
|
|
63
|
+
this.logger.info('Bot Fleet Launcher is already running');
|
|
64
|
+
return this.bots;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
this.actionCatalog = (0, catalog_1.mapActionCatalog)(configFile.actions || {});
|
|
68
|
+
this.flowCatalog = (0, mapper_2.mapFlowCatalog)(configFile.flows || {});
|
|
69
|
+
const configDir = path.dirname((0, yaml_1.getConfigPath)());
|
|
70
|
+
const flowStateDbPath = path.join(configDir, 'flows.db');
|
|
71
|
+
this.flowStateService = new state_1.FlowStateService(flowStateDbPath);
|
|
72
|
+
const flowStateTimeout = configFile.global?.sessionTimeout ?? 300;
|
|
73
|
+
this.flowExecutor = new executor_1.FlowExecutor(this.actionCatalog, this.flowCatalog, this.flowStateService, this.outboxService, flowStateTimeout, this.cooldownService);
|
|
74
|
+
this.inboxService = new inbox_1.InboxService(this.flowExecutor);
|
|
75
|
+
if (configFile.bots.length === 0) {
|
|
76
|
+
this.logger.warn('No bots configured.');
|
|
77
|
+
return this.bots;
|
|
78
|
+
}
|
|
79
|
+
const loadedBots = (0, mapper_1.mapBotsFromConfig)(configFile.bots);
|
|
80
|
+
for (const bot of loadedBots) {
|
|
81
|
+
await this.initializeBot(bot);
|
|
82
|
+
if (loadedBots.length > 1) {
|
|
83
|
+
this.logger.info('Waiting 3 seconds before initializing next bot...');
|
|
84
|
+
await this.delay(3000);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
this.isRunning = true;
|
|
88
|
+
this.logger.info(`WWeb BotForge started successfully with ${this.bots.size} bot(s)!`);
|
|
89
|
+
this.logger.info('Bots are now listening for messages...');
|
|
90
|
+
this.setupGracefulShutdown();
|
|
91
|
+
return this.bots;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
this.logger.error('Failed to start Bot Fleet Launcher:', error);
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async stop() {
|
|
99
|
+
if (!this.isRunning) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
this.logger.info('Stopping WWeb BotForge...');
|
|
103
|
+
try {
|
|
104
|
+
await this.outboxService.shutdown();
|
|
105
|
+
await this.sessionManager.removeAllChannels();
|
|
106
|
+
this.flowStateService?.close();
|
|
107
|
+
this.bots.clear();
|
|
108
|
+
this.isRunning = false;
|
|
109
|
+
this.logger.info('WWeb BotForge stopped successfully');
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
this.logger.error('Error stopping Bot Fleet:', error);
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async initializeBot(bot) {
|
|
117
|
+
try {
|
|
118
|
+
this.logger.info(`Initializing bot: ${bot.name} (${bot.id})`);
|
|
119
|
+
this.bots.set(bot.id, bot);
|
|
120
|
+
const channel = this.sessionManager.createChannel(bot.id);
|
|
121
|
+
bot.channel = channel;
|
|
122
|
+
this.outboxService.setupBotQueue(bot);
|
|
123
|
+
this.inboxService.registerBot(bot);
|
|
124
|
+
this.setupBotEventHandlers(bot);
|
|
125
|
+
await bot.channel.connect();
|
|
126
|
+
this.logger.info(`Bot "${bot.name}" initialized and ready`);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
this.logger.error(`Failed to initialize bot "${bot.name}":`, error);
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
setupBotEventHandlers(bot) {
|
|
134
|
+
if (!bot.channel) {
|
|
135
|
+
throw new Error(`Bot "${bot.name}" does not have a registered channel`);
|
|
136
|
+
}
|
|
137
|
+
bot.channel.onReady(() => {
|
|
138
|
+
this.logger.info(`Bot "${bot.name}" is ready!`);
|
|
139
|
+
});
|
|
140
|
+
bot.channel.onDisconnected((reason) => {
|
|
141
|
+
this.logger.warn(`Bot "${bot.name}" disconnected:`, reason);
|
|
142
|
+
});
|
|
143
|
+
bot.channel.onAuthFailure((error) => {
|
|
144
|
+
this.logger.error(`Bot "${bot.name}" authentication failed:`, error.message);
|
|
145
|
+
});
|
|
146
|
+
bot.channel.onConnectionError((error) => {
|
|
147
|
+
this.logger.error(`Bot "${bot.name}" connection error:`, error.message);
|
|
148
|
+
});
|
|
149
|
+
bot.channel.onStateChange((state) => {
|
|
150
|
+
this.logger.info(`Bot "${bot.name}" state changed to:`, state);
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
setupGracefulShutdown() {
|
|
154
|
+
const shutdown = async () => {
|
|
155
|
+
this.logger.info('\nReceived shutdown signal...');
|
|
156
|
+
await this.stop();
|
|
157
|
+
process.exit(0);
|
|
158
|
+
};
|
|
159
|
+
process.on('SIGINT', shutdown);
|
|
160
|
+
process.on('SIGTERM', shutdown);
|
|
161
|
+
process.on('SIGUSR2', shutdown);
|
|
162
|
+
process.on('uncaughtException', (error) => {
|
|
163
|
+
this.logger.error('Uncaught Exception:', error);
|
|
164
|
+
this.stop().finally(() => process.exit(1));
|
|
165
|
+
});
|
|
166
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
167
|
+
this.logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
|
168
|
+
this.stop().finally(() => process.exit(1));
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
getStatus() {
|
|
172
|
+
const botStatuses = Array.from(this.bots.values()).map(bot => {
|
|
173
|
+
const queueStatus = this.outboxService.getBotQueueStatus(bot.id);
|
|
174
|
+
return {
|
|
175
|
+
id: bot.id,
|
|
176
|
+
name: bot.name,
|
|
177
|
+
flowsCount: bot.flows.length,
|
|
178
|
+
queue: {
|
|
179
|
+
size: queueStatus.queueSize,
|
|
180
|
+
delayMs: queueStatus.delayMs,
|
|
181
|
+
isProcessing: queueStatus.isProcessing,
|
|
182
|
+
hasCallback: queueStatus.hasCallback,
|
|
183
|
+
},
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
const queueStatus = this.outboxService.getAllQueuesStatus();
|
|
187
|
+
return {
|
|
188
|
+
isRunning: this.isRunning,
|
|
189
|
+
bots: botStatuses,
|
|
190
|
+
totalBots: botStatuses.length,
|
|
191
|
+
queues: queueStatus,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
isRunningStatus() {
|
|
195
|
+
return this.isRunning;
|
|
196
|
+
}
|
|
197
|
+
delay(ms) {
|
|
198
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
199
|
+
}
|
|
200
|
+
getOutboxService() {
|
|
201
|
+
return this.outboxService;
|
|
202
|
+
}
|
|
203
|
+
getBots() {
|
|
204
|
+
return this.bots;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
exports.BotFleet = BotFleet;
|
package/dist/bot/bot.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createBot = createBot;
|
|
4
|
+
exports.registerChannel = registerChannel;
|
|
5
|
+
exports.createDefaultSettings = createDefaultSettings;
|
|
6
|
+
const validation_1 = require("./validation");
|
|
7
|
+
function createBot(props) {
|
|
8
|
+
(0, validation_1.validateBotId)(props.id);
|
|
9
|
+
return {
|
|
10
|
+
id: props.id,
|
|
11
|
+
phone: props.phone,
|
|
12
|
+
settings: props.settings,
|
|
13
|
+
flows: props.flows || [],
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
function registerChannel(bot, channel) {
|
|
17
|
+
bot.channel = channel;
|
|
18
|
+
return bot;
|
|
19
|
+
}
|
|
20
|
+
function createDefaultSettings() {
|
|
21
|
+
return {
|
|
22
|
+
simulateTyping: true,
|
|
23
|
+
typingDelay: 1000,
|
|
24
|
+
queueDelay: 1000,
|
|
25
|
+
readReceipts: true,
|
|
26
|
+
ignoreGroups: true,
|
|
27
|
+
ignoredSenders: [],
|
|
28
|
+
adminNumbers: [],
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
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.BotFleet = void 0;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
const mapper_1 = require("./mapper");
|
|
39
|
+
const catalog_1 = require("../action/catalog");
|
|
40
|
+
const mapper_2 = require("../flow/mapper");
|
|
41
|
+
const cooldown_1 = require("../action/cooldown");
|
|
42
|
+
const inbox_1 = require("../services/inbox");
|
|
43
|
+
const state_1 = require("../flow/state");
|
|
44
|
+
const executor_1 = require("../flow/executor");
|
|
45
|
+
const session_1 = require("../whatsapp/session");
|
|
46
|
+
const yaml_1 = require("../config/yaml");
|
|
47
|
+
const logger_1 = require("../utils/logger");
|
|
48
|
+
class BotFleet {
|
|
49
|
+
constructor(outboxService) {
|
|
50
|
+
this.bots = new Map();
|
|
51
|
+
this.actionCatalog = new Map();
|
|
52
|
+
this.flowCatalog = new Map();
|
|
53
|
+
this.isRunning = false;
|
|
54
|
+
this.sessionManager = session_1.SessionManager.getInstance();
|
|
55
|
+
this.outboxService = outboxService;
|
|
56
|
+
this.cooldownService = new cooldown_1.CooldownService();
|
|
57
|
+
}
|
|
58
|
+
get logger() {
|
|
59
|
+
return (0, logger_1.getLogger)();
|
|
60
|
+
}
|
|
61
|
+
async start(configFile) {
|
|
62
|
+
if (this.isRunning) {
|
|
63
|
+
this.logger.info('Bot Fleet Launcher is already running');
|
|
64
|
+
return this.bots;
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
this.actionCatalog = (0, catalog_1.mapActionCatalog)(configFile.actions || {});
|
|
68
|
+
this.flowCatalog = (0, mapper_2.mapFlowCatalog)(configFile.flows || {});
|
|
69
|
+
const configDir = path.dirname((0, yaml_1.getConfigPath)());
|
|
70
|
+
const flowStateDbPath = path.join(configDir, 'flows.db');
|
|
71
|
+
this.flowStateService = new state_1.FlowStateService(flowStateDbPath);
|
|
72
|
+
const flowStateTimeout = configFile.global?.sessionTimeout ?? 300;
|
|
73
|
+
this.flowExecutor = new executor_1.FlowExecutor(this.actionCatalog, this.flowCatalog, this.flowStateService, this.outboxService, flowStateTimeout, this.cooldownService);
|
|
74
|
+
this.inboxService = new inbox_1.InboxService(this.flowExecutor);
|
|
75
|
+
if (Object.keys(configFile.bots).length === 0) {
|
|
76
|
+
this.logger.warn('No bots configured.');
|
|
77
|
+
return this.bots;
|
|
78
|
+
}
|
|
79
|
+
const loadedBots = (0, mapper_1.mapBotsFromConfig)(configFile.bots);
|
|
80
|
+
for (const bot of loadedBots) {
|
|
81
|
+
await this.initializeBot(bot);
|
|
82
|
+
if (loadedBots.length > 1) {
|
|
83
|
+
this.logger.info('Waiting 3 seconds before initializing next bot...');
|
|
84
|
+
await this.delay(3000);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
this.isRunning = true;
|
|
88
|
+
this.logger.info(`WWeb BotForge started successfully with ${this.bots.size} bot(s)!`);
|
|
89
|
+
this.logger.info('Bots are now listening for messages...');
|
|
90
|
+
this.setupGracefulShutdown();
|
|
91
|
+
return this.bots;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
this.logger.error('Failed to start Bot Fleet Launcher:', error);
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async stop() {
|
|
99
|
+
if (!this.isRunning) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
this.logger.info('Stopping WWeb BotForge...');
|
|
103
|
+
try {
|
|
104
|
+
await this.outboxService.shutdown();
|
|
105
|
+
await this.sessionManager.removeAllChannels();
|
|
106
|
+
this.flowStateService?.close();
|
|
107
|
+
this.bots.clear();
|
|
108
|
+
this.isRunning = false;
|
|
109
|
+
this.logger.info('WWeb BotForge stopped successfully');
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
this.logger.error('Error stopping Bot Fleet:', error);
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
async initializeBot(bot) {
|
|
117
|
+
try {
|
|
118
|
+
this.logger.info(`Initializing bot: ${bot.id} (${bot.id})`);
|
|
119
|
+
this.bots.set(bot.id, bot);
|
|
120
|
+
const channel = this.sessionManager.createChannel(bot.id);
|
|
121
|
+
bot.channel = channel;
|
|
122
|
+
this.outboxService.setupBotQueue(bot);
|
|
123
|
+
this.inboxService.registerBot(bot);
|
|
124
|
+
this.setupBotEventHandlers(bot);
|
|
125
|
+
await bot.channel.connect();
|
|
126
|
+
this.logger.info(`Bot "${bot.id}" initialized and ready`);
|
|
127
|
+
}
|
|
128
|
+
catch (error) {
|
|
129
|
+
this.logger.error(`Failed to initialize bot "${bot.id}":`, error);
|
|
130
|
+
throw error;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
setupBotEventHandlers(bot) {
|
|
134
|
+
if (!bot.channel) {
|
|
135
|
+
throw new Error(`Bot "${bot.id}" does not have a registered channel`);
|
|
136
|
+
}
|
|
137
|
+
bot.channel.onReady(() => {
|
|
138
|
+
this.logger.info(`Bot "${bot.id}" is ready!`);
|
|
139
|
+
const phone = bot.channel.getPhone();
|
|
140
|
+
if (phone) {
|
|
141
|
+
bot.phone = phone;
|
|
142
|
+
}
|
|
143
|
+
});
|
|
144
|
+
bot.channel.onDisconnected((reason) => {
|
|
145
|
+
this.logger.warn(`Bot "${bot.id}" disconnected:`, reason);
|
|
146
|
+
});
|
|
147
|
+
bot.channel.onAuthFailure((error) => {
|
|
148
|
+
this.logger.error(`Bot "${bot.id}" authentication failed:`, error.message);
|
|
149
|
+
});
|
|
150
|
+
bot.channel.onConnectionError((error) => {
|
|
151
|
+
this.logger.error(`Bot "${bot.id}" connection error:`, error.message);
|
|
152
|
+
});
|
|
153
|
+
bot.channel.onStateChange((state) => {
|
|
154
|
+
this.logger.info(`Bot "${bot.id}" state changed to:`, state);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
setupGracefulShutdown() {
|
|
158
|
+
const shutdown = async () => {
|
|
159
|
+
this.logger.info('\nReceived shutdown signal...');
|
|
160
|
+
await this.stop();
|
|
161
|
+
process.exit(0);
|
|
162
|
+
};
|
|
163
|
+
process.on('SIGINT', shutdown);
|
|
164
|
+
process.on('SIGTERM', shutdown);
|
|
165
|
+
process.on('SIGUSR2', shutdown);
|
|
166
|
+
process.on('uncaughtException', (error) => {
|
|
167
|
+
this.logger.error('Uncaught Exception:', error);
|
|
168
|
+
this.stop().finally(() => process.exit(1));
|
|
169
|
+
});
|
|
170
|
+
process.on('unhandledRejection', (reason, promise) => {
|
|
171
|
+
this.logger.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
|
172
|
+
this.stop().finally(() => process.exit(1));
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
getStatus() {
|
|
176
|
+
const botStatuses = Array.from(this.bots.values()).map(bot => {
|
|
177
|
+
const queueStatus = this.outboxService.getBotQueueStatus(bot.id);
|
|
178
|
+
return {
|
|
179
|
+
id: bot.id,
|
|
180
|
+
name: bot.id,
|
|
181
|
+
flowsCount: bot.flows.length,
|
|
182
|
+
queue: {
|
|
183
|
+
size: queueStatus.queueSize,
|
|
184
|
+
delayMs: queueStatus.delayMs,
|
|
185
|
+
isProcessing: queueStatus.isProcessing,
|
|
186
|
+
hasCallback: queueStatus.hasCallback,
|
|
187
|
+
},
|
|
188
|
+
};
|
|
189
|
+
});
|
|
190
|
+
const queueStatus = this.outboxService.getAllQueuesStatus();
|
|
191
|
+
return {
|
|
192
|
+
isRunning: this.isRunning,
|
|
193
|
+
bots: botStatuses,
|
|
194
|
+
totalBots: botStatuses.length,
|
|
195
|
+
queues: queueStatus,
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
isRunningStatus() {
|
|
199
|
+
return this.isRunning;
|
|
200
|
+
}
|
|
201
|
+
delay(ms) {
|
|
202
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
203
|
+
}
|
|
204
|
+
getOutboxService() {
|
|
205
|
+
return this.outboxService;
|
|
206
|
+
}
|
|
207
|
+
getBots() {
|
|
208
|
+
return this.bots;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
exports.BotFleet = BotFleet;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.matchFuzzy = matchFuzzy;
|
|
7
|
+
const fuse_js_1 = __importDefault(require("fuse.js"));
|
|
8
|
+
function matchFuzzy(segments, message, threshold) {
|
|
9
|
+
if (segments.length === 0)
|
|
10
|
+
return null;
|
|
11
|
+
const fuse = new fuse_js_1.default(segments, {
|
|
12
|
+
includeScore: true,
|
|
13
|
+
threshold,
|
|
14
|
+
});
|
|
15
|
+
const results = fuse.search(message);
|
|
16
|
+
if (results.length > 0 && results[0].score !== undefined && results[0].score <= threshold) {
|
|
17
|
+
return segments[results[0].refIndex];
|
|
18
|
+
}
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.mapConfigToBot = mapConfigToBot;
|
|
4
|
+
exports.mapFlowRef = mapFlowRef;
|
|
5
|
+
exports.mapSettings = mapSettings;
|
|
6
|
+
exports.mapBotsFromConfig = mapBotsFromConfig;
|
|
7
|
+
const bot_1 = require("./bot");
|
|
8
|
+
const validation_1 = require("./validation");
|
|
9
|
+
function mapConfigToBot(id, config) {
|
|
10
|
+
(0, validation_1.validateBotId)(id);
|
|
11
|
+
const settings = config.settings ? mapSettings(config.settings) : (0, bot_1.createDefaultSettings)();
|
|
12
|
+
const flows = (config.flows || []).map(mapFlowRef);
|
|
13
|
+
return (0, bot_1.createBot)({
|
|
14
|
+
id,
|
|
15
|
+
settings,
|
|
16
|
+
flows,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function mapFlowRef(config) {
|
|
20
|
+
if (config.priority !== undefined) {
|
|
21
|
+
(0, validation_1.validatePriority)(config.priority);
|
|
22
|
+
}
|
|
23
|
+
return {
|
|
24
|
+
id: config.id,
|
|
25
|
+
priority: config.priority ?? 1,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
function mapSettings(config) {
|
|
29
|
+
if (config.typing_delay !== undefined) {
|
|
30
|
+
(0, validation_1.validateTypingDelay)(config.typing_delay);
|
|
31
|
+
}
|
|
32
|
+
if (config.queue_delay !== undefined) {
|
|
33
|
+
(0, validation_1.validateQueueDelay)(config.queue_delay);
|
|
34
|
+
}
|
|
35
|
+
return {
|
|
36
|
+
simulateTyping: config.simulate_typing ?? true,
|
|
37
|
+
typingDelay: config.typing_delay ?? 1000,
|
|
38
|
+
queueDelay: config.queue_delay ?? 1000,
|
|
39
|
+
readReceipts: config.read_receipts ?? true,
|
|
40
|
+
ignoreGroups: config.ignore_groups ?? true,
|
|
41
|
+
ignoredSenders: config.ignored_senders || [],
|
|
42
|
+
adminNumbers: config.admin_numbers || [],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function mapBotsFromConfig(configs) {
|
|
46
|
+
return Object.entries(configs).map(([id, config]) => mapConfigToBot(id, config));
|
|
47
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.validateBotId = validateBotId;
|
|
4
|
+
exports.validatePriority = validatePriority;
|
|
5
|
+
exports.validateTypingDelay = validateTypingDelay;
|
|
6
|
+
exports.validateQueueDelay = validateQueueDelay;
|
|
7
|
+
function validateBotId(id) {
|
|
8
|
+
if (!id || id.length < 3) {
|
|
9
|
+
throw new Error('Bot ID must be at least 3 characters long');
|
|
10
|
+
}
|
|
11
|
+
if (!/^[a-z0-9-]+$/.test(id)) {
|
|
12
|
+
throw new Error('Bot ID can only contain lowercase letters, numbers and hyphens');
|
|
13
|
+
}
|
|
14
|
+
if (/^[-]/.test(id)) {
|
|
15
|
+
throw new Error('Bot ID cannot start with a hyphen');
|
|
16
|
+
}
|
|
17
|
+
if (/[-]$/.test(id)) {
|
|
18
|
+
throw new Error('Bot ID cannot end with a hyphen');
|
|
19
|
+
}
|
|
20
|
+
if (/--/.test(id)) {
|
|
21
|
+
throw new Error('Bot ID cannot contain consecutive hyphens');
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function validatePriority(priority) {
|
|
25
|
+
if (priority < 0) {
|
|
26
|
+
throw new Error('Priority must be non-negative');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function validateTypingDelay(delay) {
|
|
30
|
+
if (delay < 0) {
|
|
31
|
+
throw new Error('Typing delay must be non-negative');
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function validateQueueDelay(delay) {
|
|
35
|
+
if (delay < 0) {
|
|
36
|
+
throw new Error('Queue delay must be non-negative');
|
|
37
|
+
}
|
|
38
|
+
}
|