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,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.FlowExecutor = void 0;
|
|
4
|
+
const executor_1 = require("../action/executor");
|
|
5
|
+
const template_1 = require("../action/template");
|
|
6
|
+
const webhook_1 = require("../utils/webhook");
|
|
7
|
+
const fuzzy_1 = require("../bot/fuzzy");
|
|
8
|
+
const logger_1 = require("../utils/logger");
|
|
9
|
+
class FlowExecutor {
|
|
10
|
+
constructor(actionCatalog, flowCatalog, flowStateService, outboxService, defaultTimeout = 300, cooldownService) {
|
|
11
|
+
this.actionCatalog = actionCatalog;
|
|
12
|
+
this.flowCatalog = flowCatalog;
|
|
13
|
+
this.flowStateService = flowStateService;
|
|
14
|
+
this.outboxService = outboxService;
|
|
15
|
+
this.defaultTimeout = defaultTimeout;
|
|
16
|
+
this.cooldownService = cooldownService;
|
|
17
|
+
}
|
|
18
|
+
get logger() {
|
|
19
|
+
return (0, logger_1.getLogger)();
|
|
20
|
+
}
|
|
21
|
+
async handleMessage(bot, message) {
|
|
22
|
+
this.flowStateService.cleanupExpired();
|
|
23
|
+
const state = this.flowStateService.findActive(message.from, bot.id);
|
|
24
|
+
if (state) {
|
|
25
|
+
return this.handleActiveState(bot, message, state);
|
|
26
|
+
}
|
|
27
|
+
return this.handleNewMessage(bot, message);
|
|
28
|
+
}
|
|
29
|
+
async handleActiveState(bot, message, state) {
|
|
30
|
+
const flow = this.flowCatalog.get(state.flowId);
|
|
31
|
+
if (!flow) {
|
|
32
|
+
this.flowStateService.destroy(state.id);
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
const currentStep = flow.steps[state.stepId];
|
|
36
|
+
if (!currentStep) {
|
|
37
|
+
this.flowStateService.destroy(state.id);
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
const branch = this.findMatchingBranch(currentStep.branches, message.content);
|
|
41
|
+
if (!branch) {
|
|
42
|
+
if (flow.fallbackStep && flow.steps[flow.fallbackStep]) {
|
|
43
|
+
await this.transitionToStep(bot, message, state, flow, flow.fallbackStep);
|
|
44
|
+
return true;
|
|
45
|
+
}
|
|
46
|
+
this.logger.debug(`No branch matched for flow state ${state.id}, ignoring message`);
|
|
47
|
+
return true;
|
|
48
|
+
}
|
|
49
|
+
await this.transitionToStep(bot, message, state, flow, branch.goto);
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
async handleNewMessage(bot, message) {
|
|
53
|
+
const flowRefs = [...bot.flows].sort((a, b) => b.priority - a.priority);
|
|
54
|
+
for (const flowRef of flowRefs) {
|
|
55
|
+
const flow = this.flowCatalog.get(flowRef.id);
|
|
56
|
+
if (!flow) {
|
|
57
|
+
this.logger.warn(`Flow "${flowRef.id}" referenced by bot "${bot.name}" not found`);
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
const entryStep = flow.steps[flow.entry];
|
|
61
|
+
if (!entryStep) {
|
|
62
|
+
this.logger.warn(`Entry step "${flow.entry}" for flow "${flow.id}" not found`);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (!this.matchesTriggers(entryStep, message.content)) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
const consumed = await this.executeStepAction(bot, message, entryStep, {});
|
|
69
|
+
if (!consumed) {
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
if (entryStep.branches.length === 0) {
|
|
73
|
+
this.flowStateService.destroyBySenderBot(message.from, bot.id);
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
const timeout = flow.timeout ?? this.defaultTimeout;
|
|
77
|
+
this.flowStateService.create(message.from, bot.id, flow.id, flow.entry, timeout);
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
async transitionToStep(bot, message, state, flow, stepId) {
|
|
84
|
+
const step = flow.steps[stepId];
|
|
85
|
+
if (!step) {
|
|
86
|
+
this.logger.error(`Step "${stepId}" not found in flow "${flow.id}"`);
|
|
87
|
+
this.flowStateService.destroy(state.id);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
await this.executeStepAction(bot, message, step, state.variables);
|
|
91
|
+
if (step.branches.length === 0) {
|
|
92
|
+
this.flowStateService.destroy(state.id);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
this.flowStateService.updateStep(state.id, stepId, state.variables);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
async executeStepAction(bot, message, step, variables) {
|
|
99
|
+
const context = {
|
|
100
|
+
botId: bot.id,
|
|
101
|
+
botName: bot.name,
|
|
102
|
+
sender: message.from,
|
|
103
|
+
message: message.content,
|
|
104
|
+
variables,
|
|
105
|
+
};
|
|
106
|
+
if (this.isActionOnCooldown(bot, step.action, message.from)) {
|
|
107
|
+
const action = (0, executor_1.getAction)(this.actionCatalog, step.action);
|
|
108
|
+
if (action.cooldownReply) {
|
|
109
|
+
const reply = (0, template_1.resolveVars)(action.cooldownReply, context);
|
|
110
|
+
this.outboxService.enqueue(bot.id, message.from, reply);
|
|
111
|
+
this.logger.info(`Cooldown reply sent for action "${step.action}" to ${message.from}`);
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
this.logger.warn(`Action "${step.action}" on cooldown, no cooldown_reply defined, skipping`);
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
const result = (0, executor_1.executeAction)(this.actionCatalog, step.action, context);
|
|
118
|
+
this.setActionCooldown(step.action, message.from);
|
|
119
|
+
if (result.reply) {
|
|
120
|
+
this.outboxService.enqueue(bot.id, message.from, result.reply);
|
|
121
|
+
}
|
|
122
|
+
if (result.webhook) {
|
|
123
|
+
try {
|
|
124
|
+
const payload = this.buildWebhookPayload(bot, message, result.webhook.name || 'unnamed');
|
|
125
|
+
await (0, webhook_1.sendWebhookRequest)({
|
|
126
|
+
url: result.webhook.url,
|
|
127
|
+
method: result.webhook.method,
|
|
128
|
+
headers: result.webhook.headers,
|
|
129
|
+
body: payload,
|
|
130
|
+
timeout: result.webhook.timeout,
|
|
131
|
+
retries: result.webhook.retries,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
catch (error) {
|
|
135
|
+
this.logger.error(`Failed to trigger webhook action for bot "${bot.name}":`, error);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
isActionOnCooldown(bot, actionId, sender) {
|
|
141
|
+
if (!this.cooldownService) {
|
|
142
|
+
return false;
|
|
143
|
+
}
|
|
144
|
+
const action = (0, executor_1.getAction)(this.actionCatalog, actionId);
|
|
145
|
+
if (!action.cooldown || action.cooldown <= 0) {
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
const cooldownMs = action.cooldown * 1000;
|
|
149
|
+
const cooldownKey = `action:${actionId}`;
|
|
150
|
+
return this.cooldownService.isOnCooldown(sender, cooldownKey, cooldownMs);
|
|
151
|
+
}
|
|
152
|
+
setActionCooldown(actionId, sender) {
|
|
153
|
+
if (!this.cooldownService) {
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const action = (0, executor_1.getAction)(this.actionCatalog, actionId);
|
|
157
|
+
if (!action.cooldown || action.cooldown <= 0) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
this.cooldownService.setCooldown(sender, `action:${actionId}`);
|
|
161
|
+
}
|
|
162
|
+
findMatchingBranch(branches, message) {
|
|
163
|
+
const defaultBranch = branches.find(branch => !branch.when || branch.when.length === 0);
|
|
164
|
+
const conditionalBranches = branches.filter(branch => branch.when && branch.when.length > 0);
|
|
165
|
+
for (const branch of conditionalBranches) {
|
|
166
|
+
const threshold = branch.fuzzyThreshold ?? 0.6;
|
|
167
|
+
if ((0, fuzzy_1.matchFuzzy)(branch.when, message, threshold)) {
|
|
168
|
+
return branch;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return defaultBranch || null;
|
|
172
|
+
}
|
|
173
|
+
matchesTriggers(step, message) {
|
|
174
|
+
if (!step.triggers || step.triggers.length === 0) {
|
|
175
|
+
return false;
|
|
176
|
+
}
|
|
177
|
+
return step.triggers.some(trigger => {
|
|
178
|
+
const threshold = trigger.fuzzyThreshold ?? 0.6;
|
|
179
|
+
return (0, fuzzy_1.matchFuzzy)(trigger.phrases, message, threshold) !== null;
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
buildWebhookPayload(bot, message, webhookName) {
|
|
183
|
+
return {
|
|
184
|
+
sender: message.from,
|
|
185
|
+
message: message.content,
|
|
186
|
+
timestamp: message.timestamp.toISOString(),
|
|
187
|
+
botId: bot.id,
|
|
188
|
+
botName: bot.name,
|
|
189
|
+
webhookName,
|
|
190
|
+
webhookPattern: '',
|
|
191
|
+
metadata: message.metadata || {},
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
exports.FlowExecutor = FlowExecutor;
|
|
@@ -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.FlowStateService = void 0;
|
|
7
|
+
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
8
|
+
class FlowStateService {
|
|
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 flow_states (
|
|
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_flow_states_sender_bot ON flow_states(sender, bot_id);
|
|
28
|
+
CREATE INDEX IF NOT EXISTS idx_flow_states_last_activity ON flow_states(last_activity_at);
|
|
29
|
+
`);
|
|
30
|
+
}
|
|
31
|
+
findActive(sender, botId, now = Date.now()) {
|
|
32
|
+
const row = this.db
|
|
33
|
+
.prepare('SELECT * FROM flow_states WHERE sender = ? AND bot_id = ?')
|
|
34
|
+
.get(sender, botId);
|
|
35
|
+
if (!row) {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const state = this.rowToState(row);
|
|
39
|
+
if (this.isExpired(state, now)) {
|
|
40
|
+
this.destroy(state.id);
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
return state;
|
|
44
|
+
}
|
|
45
|
+
create(sender, botId, flowId, stepId, timeout, now = Date.now()) {
|
|
46
|
+
this.destroyBySenderBot(sender, botId);
|
|
47
|
+
const id = `${botId}-${sender}-${now}`;
|
|
48
|
+
const state = {
|
|
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 flow_states (id, sender, bot_id, flow_id, step_id, variables, started_at, last_activity_at, timeout) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)')
|
|
61
|
+
.run(state.id, state.sender, state.botId, state.flowId, state.stepId, JSON.stringify(state.variables), state.startedAt, state.lastActivityAt, state.timeout);
|
|
62
|
+
return state;
|
|
63
|
+
}
|
|
64
|
+
updateStep(id, stepId, variables, now = Date.now()) {
|
|
65
|
+
const state = this.findById(id);
|
|
66
|
+
if (!state) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
const mergedVariables = variables ? { ...state.variables, ...variables } : state.variables;
|
|
70
|
+
this.db
|
|
71
|
+
.prepare('UPDATE flow_states 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 flow_states WHERE id = ?').run(id);
|
|
76
|
+
}
|
|
77
|
+
destroyBySenderBot(sender, botId) {
|
|
78
|
+
this.db.prepare('DELETE FROM flow_states 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 flow_states WHERE (last_activity_at + (timeout * 1000)) < ?')
|
|
84
|
+
.run(cutoff);
|
|
85
|
+
return result.changes;
|
|
86
|
+
}
|
|
87
|
+
count() {
|
|
88
|
+
const row = this.db.prepare('SELECT COUNT(*) as count FROM flow_states').get();
|
|
89
|
+
return row.count;
|
|
90
|
+
}
|
|
91
|
+
close() {
|
|
92
|
+
this.db.close();
|
|
93
|
+
}
|
|
94
|
+
findById(id) {
|
|
95
|
+
const row = this.db.prepare('SELECT * FROM flow_states WHERE id = ?').get(id);
|
|
96
|
+
return row ? this.rowToState(row) : null;
|
|
97
|
+
}
|
|
98
|
+
rowToState(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(state, now) {
|
|
112
|
+
if (state.timeout <= 0) {
|
|
113
|
+
return false;
|
|
114
|
+
}
|
|
115
|
+
return now > state.lastActivityAt + state.timeout * 1000;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
exports.FlowStateService = FlowStateService;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.InboxService = void 0;
|
|
4
|
+
const logger_1 = require("../utils/logger");
|
|
5
|
+
class InboxService {
|
|
6
|
+
constructor(flowExecutor) {
|
|
7
|
+
this.flowExecutor = flowExecutor;
|
|
8
|
+
}
|
|
9
|
+
get logger() {
|
|
10
|
+
return (0, logger_1.getLogger)();
|
|
11
|
+
}
|
|
12
|
+
registerBot(bot) {
|
|
13
|
+
if (!bot.channel) {
|
|
14
|
+
throw new Error(`Bot "${bot.id}" does not have a registered channel`);
|
|
15
|
+
}
|
|
16
|
+
bot.channel.onMessage((message) => {
|
|
17
|
+
this.handleIncomingMessage(bot, message);
|
|
18
|
+
});
|
|
19
|
+
bot.channel.onReady(() => {
|
|
20
|
+
this.logger.info(`Bot "${bot.id}" is ready and listening for messages`);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
async handleIncomingMessage(bot, message) {
|
|
24
|
+
this.logger.info(`Message received for bot "${bot.id}": ${message.content.substring(0, 50)}...`);
|
|
25
|
+
try {
|
|
26
|
+
if (message.metadata?.fromMe) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
if (this.isSenderIgnored(bot, message.from)) {
|
|
30
|
+
this.logger.debug(`Ignoring message from "${message.from}" for bot "${bot.id}" (sender in ignored list)`);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
if (bot.settings.ignoreGroups && this.isGroupMessage(message.from)) {
|
|
34
|
+
this.logger.debug(`Ignoring group message for bot "${bot.id}"`);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
await this.flowExecutor.handleMessage(bot, message);
|
|
38
|
+
}
|
|
39
|
+
catch (error) {
|
|
40
|
+
this.logger.error(`Error handling message for bot "${bot.id}":`, error);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
isSenderIgnored(bot, sender) {
|
|
44
|
+
return bot.settings.ignoredSenders.includes(sender);
|
|
45
|
+
}
|
|
46
|
+
isGroupMessage(from) {
|
|
47
|
+
return from.includes('g.us');
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
exports.InboxService = InboxService;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MessageHandlerService = void 0;
|
|
4
|
+
const auto_response_1 = require("./auto-response");
|
|
5
|
+
const webhook_1 = require("./webhook");
|
|
6
|
+
const logger_1 = require("../utils/logger");
|
|
7
|
+
class MessageHandlerService {
|
|
8
|
+
constructor(cooldownService) {
|
|
9
|
+
this.bots = new Map();
|
|
10
|
+
this.messageHandlers = new Map();
|
|
11
|
+
this.autoResponseService = new auto_response_1.AutoResponseService(cooldownService);
|
|
12
|
+
this.webhookService = new webhook_1.WebhookService(cooldownService);
|
|
13
|
+
}
|
|
14
|
+
get logger() {
|
|
15
|
+
return (0, logger_1.getLogger)();
|
|
16
|
+
}
|
|
17
|
+
registerBot(bot) {
|
|
18
|
+
if (!bot.channel) {
|
|
19
|
+
throw new Error(`Bot "${bot.name}" does not have a registered channel`);
|
|
20
|
+
}
|
|
21
|
+
this.bots.set(bot.id, bot);
|
|
22
|
+
bot.channel.onMessage((message) => {
|
|
23
|
+
this.handleIncomingMessage(bot, message);
|
|
24
|
+
});
|
|
25
|
+
bot.channel.onReady(() => {
|
|
26
|
+
this.logger.info(`π€ Bot "${bot.name}" (${bot.id}) is ready and listening for messages`);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
unregisterBot(botId) {
|
|
30
|
+
this.bots.delete(botId);
|
|
31
|
+
this.messageHandlers.delete(botId);
|
|
32
|
+
}
|
|
33
|
+
registerMessageHandler(botId, handler) {
|
|
34
|
+
this.messageHandlers.set(botId, handler);
|
|
35
|
+
}
|
|
36
|
+
async handleIncomingMessage(bot, message) {
|
|
37
|
+
this.logger.info(`π¨ Message received for bot "${bot.name}": ${message.content.substring(0, 50)}...`);
|
|
38
|
+
try {
|
|
39
|
+
if (this.isSenderIgnored(bot, message.from)) {
|
|
40
|
+
this.logger.debug(`π« Ignoring message from "${message.from}" for bot "${bot.name}" (sender in ignored list)`);
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (!this.autoResponseService.shouldProcessMessage(bot, message)) {
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const autoResponse = this.autoResponseService.processMessage(bot, message);
|
|
47
|
+
if (autoResponse) {
|
|
48
|
+
this.autoResponseService.sendAutoResponse({ enqueue: () => '' }, bot, message, autoResponse);
|
|
49
|
+
}
|
|
50
|
+
this.webhookService.processWebhooks(bot, message).catch(error => {
|
|
51
|
+
this.logger.error(`β Error processing webhooks for bot "${bot.name}":`, error);
|
|
52
|
+
});
|
|
53
|
+
const customHandler = this.messageHandlers.get(bot.id);
|
|
54
|
+
if (customHandler) {
|
|
55
|
+
customHandler(bot, message, autoResponse?.response);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
this.logger.error(`β Error handling message for bot "${bot.name}":`, error);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
getRegisteredBots() {
|
|
63
|
+
return Array.from(this.bots.values());
|
|
64
|
+
}
|
|
65
|
+
isBotRegistered(botId) {
|
|
66
|
+
return this.bots.has(botId);
|
|
67
|
+
}
|
|
68
|
+
getBot(botId) {
|
|
69
|
+
return this.bots.get(botId);
|
|
70
|
+
}
|
|
71
|
+
isSenderIgnored(bot, sender) {
|
|
72
|
+
return bot.settings.ignoredSenders.includes(sender);
|
|
73
|
+
}
|
|
74
|
+
getAutoResponseService() {
|
|
75
|
+
return this.autoResponseService;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
exports.MessageHandlerService = MessageHandlerService;
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.MessageQueueService = void 0;
|
|
4
|
+
const logger_1 = require("../utils/logger");
|
|
5
|
+
class MessageQueueService {
|
|
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.name}" 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.name}": 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 message queue 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('β
Message queue service shut down');
|
|
133
|
+
}
|
|
134
|
+
delay(ms) {
|
|
135
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
exports.MessageQueueService = MessageQueueService;
|