botforje 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +381 -0
  3. package/dist/action/action.js +44 -0
  4. package/dist/action/catalog.js +40 -0
  5. package/dist/action/cooldown.js +60 -0
  6. package/dist/action/executor.js +23 -0
  7. package/dist/action/template.js +13 -0
  8. package/dist/action/types.js +2 -0
  9. package/dist/action/webhook.js +36 -0
  10. package/dist/actions/action.js +26 -0
  11. package/dist/actions/cooldown.js +66 -0
  12. package/dist/actions/request.js +40 -0
  13. package/dist/api/routes/auth.js +56 -0
  14. package/dist/api/routes/bots.js +51 -0
  15. package/dist/api/routes/config.js +24 -0
  16. package/dist/api/routes/health.js +15 -0
  17. package/dist/api/routes/index.js +14 -0
  18. package/dist/api/routes/messages.js +69 -0
  19. package/dist/api/routes/sessions.js +97 -0
  20. package/dist/api/routes/status.js +38 -0
  21. package/dist/api/server.js +153 -0
  22. package/dist/auth/service.js +102 -0
  23. package/dist/boot/fleet.js +207 -0
  24. package/dist/bot/bot.js +30 -0
  25. package/dist/bot/fleet.js +211 -0
  26. package/dist/bot/fuzzy.js +20 -0
  27. package/dist/bot/mapper.js +47 -0
  28. package/dist/bot/types.js +2 -0
  29. package/dist/bot/validation.js +38 -0
  30. package/dist/bot.js +31 -0
  31. package/dist/cli/create-bot.js +84 -0
  32. package/dist/cli.js +138 -0
  33. package/dist/commands/auth.js +200 -0
  34. package/dist/commands/create-bot.js +64 -0
  35. package/dist/commands/guide.js +563 -0
  36. package/dist/commands/lock.js +73 -0
  37. package/dist/commands/status.js +145 -0
  38. package/dist/commands/unlock.js +88 -0
  39. package/dist/commands/validate.js +19 -0
  40. package/dist/config/mapper.js +152 -0
  41. package/dist/config/schema.js +2 -0
  42. package/dist/config/validation.js +705 -0
  43. package/dist/config/watcher.js +145 -0
  44. package/dist/config/yaml.js +197 -0
  45. package/dist/fleet.js +247 -0
  46. package/dist/flow/executor.js +286 -0
  47. package/dist/flow/flow.js +2 -0
  48. package/dist/flow/mapper.js +72 -0
  49. package/dist/flow/state.js +115 -0
  50. package/dist/flow/types.js +2 -0
  51. package/dist/graph/executor.js +294 -0
  52. package/dist/graph/graph.js +2 -0
  53. package/dist/graph/state.js +118 -0
  54. package/dist/helpers/data.js +42 -0
  55. package/dist/helpers/fuzzy.js +30 -0
  56. package/dist/helpers/logger.js +89 -0
  57. package/dist/helpers/validation.js +38 -0
  58. package/dist/index.js +92 -0
  59. package/dist/messages/contracts.js +2 -0
  60. package/dist/messages/inbox.js +58 -0
  61. package/dist/messages/outbox.js +136 -0
  62. package/dist/services/auto-response.js +62 -0
  63. package/dist/services/cooldown.js +60 -0
  64. package/dist/services/fleet.js +207 -0
  65. package/dist/services/flow-executor.js +195 -0
  66. package/dist/services/flow-state.js +118 -0
  67. package/dist/services/inbox.js +50 -0
  68. package/dist/services/message-handler.js +78 -0
  69. package/dist/services/message-queue.js +138 -0
  70. package/dist/services/outbox.js +138 -0
  71. package/dist/services/session.js +118 -0
  72. package/dist/services/webhook.js +87 -0
  73. package/dist/utils/logger.js +89 -0
  74. package/dist/utils/webhook.js +36 -0
  75. package/dist/validation/validate.js +592 -0
  76. package/dist/whatsapp/client.js +293 -0
  77. package/dist/whatsapp/session.js +158 -0
  78. package/dist/whatsapp/types.js +109 -0
  79. package/dist/whatsapp/whatsapp.js +109 -0
  80. package/package.json +97 -0
  81. package/scripts/postinstall.js +45 -0
  82. package/scripts/preuninstall.js +48 -0
  83. package/scripts/setup-systemd.js +91 -0
  84. package/service/botforje.service.template +25 -0
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.OutboxService = void 0;
4
+ const logger_1 = require("../utils/logger");
5
+ class OutboxService {
6
+ constructor() {
7
+ this.queues = new Map();
8
+ this.processing = new Map();
9
+ this.delays = new Map();
10
+ this.sendCallbacks = new Map();
11
+ }
12
+ get logger() {
13
+ return (0, logger_1.getLogger)();
14
+ }
15
+ setBotDelay(botId, delayMs) {
16
+ this.delays.set(botId, delayMs);
17
+ this.logger.info(`Queue delay for bot "${botId}" set to ${delayMs}ms`);
18
+ }
19
+ setBotSendCallback(botId, callback) {
20
+ this.sendCallbacks.set(botId, callback);
21
+ }
22
+ setupBotQueue(bot) {
23
+ if (!bot.channel) {
24
+ throw new Error(`Bot "${bot.id}" does not have a registered channel`);
25
+ }
26
+ const botId = bot.id;
27
+ this.setBotDelay(botId, bot.settings.queueDelay);
28
+ this.setBotSendCallback(botId, async (botId, message) => {
29
+ await bot.channel.send(message);
30
+ });
31
+ this.logger.info(`Configured message queue for bot "${bot.id}": delay=${bot.settings.queueDelay}ms`);
32
+ }
33
+ enqueue(botId, to, content, metadata) {
34
+ const messageId = `${botId}-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
35
+ this.logger.debug(`Queueing message for bot "${botId}" with metadata:`, metadata);
36
+ const outgoingMessage = {
37
+ to,
38
+ content,
39
+ metadata,
40
+ };
41
+ const queuedMessage = {
42
+ id: messageId,
43
+ botId,
44
+ message: outgoingMessage,
45
+ timestamp: Date.now(),
46
+ };
47
+ if (!this.queues.has(botId)) {
48
+ this.queues.set(botId, []);
49
+ }
50
+ const botQueue = this.queues.get(botId);
51
+ botQueue.push(queuedMessage);
52
+ this.logger.info(`Message queued for bot "${botId}": ${messageId} (queue size: ${botQueue.length})`);
53
+ if (!this.processing.get(botId)) {
54
+ this.startProcessing(botId);
55
+ }
56
+ return messageId;
57
+ }
58
+ async startProcessing(botId) {
59
+ if (this.processing.get(botId)) {
60
+ return;
61
+ }
62
+ this.processing.set(botId, true);
63
+ this.logger.info(`Started processing queue for bot "${botId}"`);
64
+ const botQueue = this.queues.get(botId);
65
+ if (!botQueue) {
66
+ this.processing.set(botId, false);
67
+ return;
68
+ }
69
+ while (botQueue.length > 0) {
70
+ const messageData = botQueue.shift();
71
+ const delay = this.delays.get(botId) || 2000;
72
+ try {
73
+ this.logger.debug(`Waiting ${delay}ms before sending message ${messageData.id} from bot "${botId}"...`);
74
+ await this.delay(delay);
75
+ this.logger.info(`Processing queued message: ${messageData.id} from bot "${botId}"`);
76
+ const callback = this.sendCallbacks.get(botId);
77
+ if (callback) {
78
+ await callback(botId, messageData.message);
79
+ this.logger.info(`Queued message sent successfully: ${messageData.id}`);
80
+ }
81
+ else {
82
+ this.logger.error(`No send callback configured for bot "${botId}"`);
83
+ }
84
+ }
85
+ catch (error) {
86
+ this.logger.error(`Error sending queued message ${messageData.id} from bot "${botId}":`, error);
87
+ }
88
+ }
89
+ this.processing.set(botId, false);
90
+ this.logger.info(`Queue processing completed for bot "${botId}"`);
91
+ }
92
+ getBotQueueStatus(botId) {
93
+ const queue = this.queues.get(botId) || [];
94
+ const isProcessing = this.processing.get(botId) || false;
95
+ const delay = this.delays.get(botId) || 2000;
96
+ return {
97
+ botId,
98
+ queueSize: queue.length,
99
+ isProcessing,
100
+ delayMs: delay,
101
+ hasCallback: this.sendCallbacks.has(botId),
102
+ nextMessage: queue.length > 0 ? {
103
+ id: queue[0].id,
104
+ to: queue[0].message.to,
105
+ age: Date.now() - queue[0].timestamp,
106
+ } : null,
107
+ };
108
+ }
109
+ clearBotQueue(botId) {
110
+ this.queues.delete(botId);
111
+ this.logger.info(`Queue cleared for bot "${botId}"`);
112
+ }
113
+ getAllQueuesStatus() {
114
+ const allStatuses = [];
115
+ for (const [botId] of this.queues.entries()) {
116
+ const status = this.getBotQueueStatus(botId);
117
+ allStatuses.push(status);
118
+ }
119
+ return {
120
+ totalQueues: allStatuses.length,
121
+ queues: allStatuses,
122
+ };
123
+ }
124
+ async shutdown() {
125
+ this.logger.info('Shutting down outbox service...');
126
+ for (const [botId] of this.processing.entries()) {
127
+ this.processing.set(botId, false);
128
+ }
129
+ this.queues.clear();
130
+ this.delays.clear();
131
+ this.sendCallbacks.clear();
132
+ this.logger.info('Outbox service shut down');
133
+ }
134
+ delay(ms) {
135
+ return new Promise(resolve => setTimeout(resolve, ms));
136
+ }
137
+ }
138
+ exports.OutboxService = OutboxService;
@@ -0,0 +1,118 @@
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.SessionService = void 0;
7
+ const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
8
+ class SessionService {
9
+ constructor(dbPath) {
10
+ this.db = new better_sqlite3_1.default(dbPath);
11
+ this.setupSchema();
12
+ }
13
+ setupSchema() {
14
+ this.db.exec(`
15
+ CREATE TABLE IF NOT EXISTS sessions (
16
+ id TEXT PRIMARY KEY,
17
+ sender TEXT NOT NULL,
18
+ bot_id TEXT NOT NULL,
19
+ flow_id TEXT NOT NULL,
20
+ step_id TEXT NOT NULL,
21
+ variables TEXT NOT NULL DEFAULT '{}',
22
+ started_at INTEGER NOT NULL,
23
+ last_activity_at INTEGER NOT NULL,
24
+ timeout INTEGER NOT NULL
25
+ );
26
+
27
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_sessions_sender_bot ON sessions(sender, bot_id);
28
+ CREATE INDEX IF NOT EXISTS idx_sessions_last_activity ON sessions(last_activity_at);
29
+ `);
30
+ }
31
+ findActive(sender, botId, now = Date.now()) {
32
+ const row = this.db
33
+ .prepare('SELECT * FROM sessions WHERE sender = ? AND bot_id = ?')
34
+ .get(sender, botId);
35
+ if (!row) {
36
+ return null;
37
+ }
38
+ const session = this.rowToSession(row);
39
+ if (this.isExpired(session, now)) {
40
+ this.destroy(session.id);
41
+ return null;
42
+ }
43
+ return session;
44
+ }
45
+ create(sender, botId, flowId, stepId, timeout, now = Date.now()) {
46
+ this.destroyBySenderBot(sender, botId);
47
+ const id = `${botId}-${sender}-${now}`;
48
+ const session = {
49
+ id,
50
+ sender,
51
+ botId,
52
+ flowId,
53
+ stepId,
54
+ variables: {},
55
+ startedAt: now,
56
+ lastActivityAt: now,
57
+ timeout,
58
+ };
59
+ this.db
60
+ .prepare('INSERT INTO sessions (id, sender, bot_id, flow_id, step_id, variables, started_at, last_activity_at, timeout) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)')
61
+ .run(session.id, session.sender, session.botId, session.flowId, session.stepId, JSON.stringify(session.variables), session.startedAt, session.lastActivityAt, session.timeout);
62
+ return session;
63
+ }
64
+ updateStep(id, stepId, variables, now = Date.now()) {
65
+ const session = this.findById(id);
66
+ if (!session) {
67
+ return;
68
+ }
69
+ const mergedVariables = variables ? { ...session.variables, ...variables } : session.variables;
70
+ this.db
71
+ .prepare('UPDATE sessions SET step_id = ?, variables = ?, last_activity_at = ? WHERE id = ?')
72
+ .run(stepId, JSON.stringify(mergedVariables), now, id);
73
+ }
74
+ destroy(id) {
75
+ this.db.prepare('DELETE FROM sessions WHERE id = ?').run(id);
76
+ }
77
+ destroyBySenderBot(sender, botId) {
78
+ this.db.prepare('DELETE FROM sessions WHERE sender = ? AND bot_id = ?').run(sender, botId);
79
+ }
80
+ cleanupExpired(now = Date.now()) {
81
+ const cutoff = now;
82
+ const result = this.db
83
+ .prepare('DELETE FROM sessions WHERE (last_activity_at + (timeout * 1000)) < ?')
84
+ .run(cutoff);
85
+ return result.changes;
86
+ }
87
+ close() {
88
+ this.db.close();
89
+ }
90
+ count() {
91
+ const row = this.db.prepare('SELECT COUNT(*) as count FROM sessions').get();
92
+ return row.count;
93
+ }
94
+ findById(id) {
95
+ const row = this.db.prepare('SELECT * FROM sessions WHERE id = ?').get(id);
96
+ return row ? this.rowToSession(row) : null;
97
+ }
98
+ rowToSession(row) {
99
+ return {
100
+ id: row.id,
101
+ sender: row.sender,
102
+ botId: row.bot_id,
103
+ flowId: row.flow_id,
104
+ stepId: row.step_id,
105
+ variables: JSON.parse(row.variables || '{}'),
106
+ startedAt: row.started_at,
107
+ lastActivityAt: row.last_activity_at,
108
+ timeout: row.timeout,
109
+ };
110
+ }
111
+ isExpired(session, now) {
112
+ if (session.timeout <= 0) {
113
+ return false;
114
+ }
115
+ return now > session.lastActivityAt + session.timeout * 1000;
116
+ }
117
+ }
118
+ exports.SessionService = SessionService;
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.WebhookService = void 0;
4
+ const fuzzy_1 = require("../bot/fuzzy");
5
+ const logger_1 = require("../utils/logger");
6
+ class WebhookService {
7
+ constructor(cooldownService) {
8
+ this.cooldownService = cooldownService;
9
+ }
10
+ get logger() {
11
+ return (0, logger_1.getLogger)();
12
+ }
13
+ async processWebhooks(bot, message) {
14
+ if (message.metadata?.fromMe) {
15
+ return;
16
+ }
17
+ const sorted = [...bot.webhooks].sort((a, b) => b.priority - a.priority);
18
+ const matchingWebhooks = sorted.filter(webhook => (0, fuzzy_1.matchFuzzy)(webhook.fuzzySegments, message.content, webhook.fuzzyThreshold));
19
+ if (matchingWebhooks.length === 0) {
20
+ return;
21
+ }
22
+ for (const webhook of matchingWebhooks) {
23
+ try {
24
+ await this.triggerWebhook(bot, message, webhook);
25
+ }
26
+ catch (error) {
27
+ this.logger.error(`❌ Failed to trigger webhook "${webhook.name}" for bot "${bot.name}":`, error);
28
+ }
29
+ }
30
+ }
31
+ async triggerWebhook(bot, message, webhook) {
32
+ const cooldownMs = (webhook.cooldown || 0) * 1000;
33
+ const senderKey = message.from;
34
+ if (this.cooldownService.isOnCooldown(senderKey, webhook.name, cooldownMs)) {
35
+ this.logger.debug(`⏳ Webhook cooldown active for sender "${senderKey}" on webhook "${webhook.name}" in bot "${bot.name}"`);
36
+ return;
37
+ }
38
+ this.cooldownService.setCooldown(senderKey, webhook.name);
39
+ this.logger.info(`🔗 Triggering webhook "${webhook.name}" for bot "${bot.name}": ${webhook.method} ${webhook.url}`);
40
+ const payload = this.buildWebhookPayload(bot, message, webhook);
41
+ await this.makeHttpRequest(webhook, payload);
42
+ }
43
+ buildWebhookPayload(bot, message, webhook) {
44
+ return {
45
+ sender: message.from,
46
+ message: message.content,
47
+ timestamp: message.timestamp.toISOString(),
48
+ botId: bot.id,
49
+ botName: bot.name,
50
+ webhookName: webhook.name,
51
+ webhookPattern: webhook.patternString,
52
+ metadata: message.metadata || {},
53
+ };
54
+ }
55
+ async makeHttpRequest(webhook, payload) {
56
+ const maxRetries = webhook.retries || 3;
57
+ let lastError = null;
58
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
59
+ try {
60
+ const response = await fetch(webhook.url, {
61
+ method: webhook.method || 'POST',
62
+ headers: {
63
+ 'Content-Type': 'application/json',
64
+ ...(webhook.headers || {}),
65
+ },
66
+ body: JSON.stringify(payload),
67
+ signal: AbortSignal.timeout(webhook.timeout || 5000),
68
+ });
69
+ if (!response.ok) {
70
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
71
+ }
72
+ this.logger.info(`✅ Webhook request successful: ${webhook.url}`);
73
+ return;
74
+ }
75
+ catch (error) {
76
+ lastError = error instanceof Error ? error : new Error(String(error));
77
+ if (attempt < maxRetries) {
78
+ const backoffMs = Math.pow(2, attempt - 1) * 1000;
79
+ this.logger.warn(`⚠️ Webhook request failed (attempt ${attempt}/${maxRetries}), retrying in ${backoffMs}ms:`, lastError.message);
80
+ await new Promise(resolve => setTimeout(resolve, backoffMs));
81
+ }
82
+ }
83
+ }
84
+ throw new Error(`Webhook request failed after ${maxRetries} attempts: ${lastError?.message}`);
85
+ }
86
+ }
87
+ exports.WebhookService = WebhookService;
@@ -0,0 +1,89 @@
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.defaultLogger = void 0;
7
+ exports.createLogger = createLogger;
8
+ exports.getLogger = getLogger;
9
+ exports.setGlobalLogger = setGlobalLogger;
10
+ const pino_1 = __importDefault(require("pino"));
11
+ function getCallerInfo() {
12
+ const isDevelopment = process.env.NODE_ENV !== 'production';
13
+ if (!isDevelopment)
14
+ return undefined;
15
+ const originalPrepareStackTrace = Error.prepareStackTrace;
16
+ try {
17
+ const err = new Error();
18
+ Error.prepareStackTrace = (_, stack) => stack;
19
+ const stack = err.stack;
20
+ const caller = stack[2];
21
+ if (caller) {
22
+ const fileName = caller.getFileName() || 'unknown';
23
+ const lineNumber = caller.getLineNumber() || 0;
24
+ const functionName = caller.getFunctionName() || 'anonymous';
25
+ const shortFileName = fileName.split('/').pop() || fileName;
26
+ return {
27
+ file: shortFileName,
28
+ line: lineNumber,
29
+ func: functionName,
30
+ };
31
+ }
32
+ }
33
+ catch (e) {
34
+ }
35
+ finally {
36
+ Error.prepareStackTrace = originalPrepareStackTrace;
37
+ }
38
+ return undefined;
39
+ }
40
+ function createLogger(logLevel = 'info') {
41
+ const isDevelopment = process.env.NODE_ENV !== 'production';
42
+ const logger = (0, pino_1.default)({
43
+ level: logLevel,
44
+ transport: isDevelopment
45
+ ? {
46
+ target: 'pino-pretty',
47
+ options: {
48
+ colorize: true,
49
+ translateTime: 'SYS:yyyy-mm-dd HH:MM:ss',
50
+ ignore: 'pid,hostname,caller',
51
+ messageFormat: '{if caller}[{caller.file}:{caller.line}]{end} {msg}',
52
+ singleLine: false,
53
+ hideObject: true,
54
+ },
55
+ }
56
+ : undefined,
57
+ formatters: {
58
+ level: label => ({ level: label }),
59
+ },
60
+ timestamp: pino_1.default.stdTimeFunctions.isoTime,
61
+ });
62
+ return {
63
+ debug: (message, ...args) => {
64
+ const caller = getCallerInfo();
65
+ logger.debug({ ...(args.length > 0 ? { args } : {}), ...(caller ? { caller } : {}) }, message);
66
+ },
67
+ info: (message, ...args) => {
68
+ const caller = getCallerInfo();
69
+ logger.info({ ...(args.length > 0 ? { args } : {}), ...(caller ? { caller } : {}) }, message);
70
+ },
71
+ warn: (message, ...args) => {
72
+ const caller = getCallerInfo();
73
+ logger.warn({ ...(args.length > 0 ? { args } : {}), ...(caller ? { caller } : {}) }, message);
74
+ },
75
+ error: (message, ...args) => {
76
+ const caller = getCallerInfo();
77
+ logger.error({ ...(args.length > 0 ? { args } : {}), ...(caller ? { caller } : {}) }, message);
78
+ },
79
+ };
80
+ }
81
+ let globalLogger = createLogger('info');
82
+ function getLogger() {
83
+ return globalLogger;
84
+ }
85
+ function setGlobalLogger(config) {
86
+ const logLevel = config?.logLevel || 'info';
87
+ globalLogger = createLogger(logLevel);
88
+ }
89
+ exports.defaultLogger = createLogger('info');
@@ -0,0 +1,36 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.sendWebhookRequest = sendWebhookRequest;
4
+ const logger_1 = require("./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
+ }