-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathwhatsapp-bot.js
More file actions
203 lines (175 loc) · 6.78 KB
/
whatsapp-bot.js
File metadata and controls
203 lines (175 loc) · 6.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
#!/usr/bin/env node
/**
* WhatsApp Bot for Jellyseerr Movie/TV Requests
*
* Modernized architecture using command framework, middleware, and Fastify server
*/
import { createHttpClient } from './lib/request.js';
import { createWahaClient, getPhoneNumberByLid } from './lib/waha-client.js';
import { loadConfig, isLidFormat, setLidMapping, isPhoneNumberConfigured } from './lib/utils.js';
import { createLogger } from './lib/logger.js';
import { createServer } from './lib/server.js';
import { createStateManager } from './lib/state/cache-state.js';
import { createQueueManager, getQueueManager } from './lib/queue/message-queue.js';
import { createSubscriptionManager } from './lib/subscriptions/subscription-manager.js';
import { createCommandRegistry, getCommandRegistry } from './lib/commands/index.js';
import { createMiddlewarePipeline } from './lib/middleware/index.js';
import { sendMessage } from './lib/waha-client.js';
import { getErrorDetails } from './lib/errors/error-formatter.js';
/**
* Handles incoming WhatsApp messages using command framework and middleware
*/
async function handleMessage(cfg, jellyseerrClient, wahaClient, webhookData) {
const logger = cfg.__logger;
try {
// Extract message data from WAHA webhook payload
const payload = webhookData.payload;
if (!payload) {
logger?.warn('Webhook received with no payload', { keys: Object.keys(webhookData || {}) });
return;
}
// Only process incoming messages (not sent by us)
if (payload.fromMe) {
logger?.debug('Ignoring message from self (fromMe: true)');
return;
}
const chatId = payload.from;
const messageText = payload.body || '';
const messageId = payload.id;
// Extract timestamp from payload (WhatsApp timestamps are in seconds, convert to ms)
const messageTimestamp = payload.timestamp
? (payload.timestamp < 1000000000000 ? payload.timestamp * 1000 : payload.timestamp)
: Date.now(); // Fallback to current time if timestamp missing
// Auto-create LID mapping if we receive LID format and can resolve it
if (isLidFormat(chatId)) {
const resolvedPhoneChatId = await getPhoneNumberByLid(wahaClient, cfg, chatId);
if (resolvedPhoneChatId) {
setLidMapping(cfg, resolvedPhoneChatId, chatId, logger);
}
}
// Check if phone number is configured - ignore messages from non-configured numbers
const isConfigured = await isPhoneNumberConfigured(cfg, chatId, wahaClient);
if (!isConfigured) {
logger?.info('💬 (Ignored - user not configured)');
return;
}
// Create context for middleware and commands
const context = {
cfg,
chatId,
messageId,
messageText,
messageTimestamp, // Add timestamp for race condition prevention
jellyseerrClient,
wahaClient,
logger,
skip: false,
error: null
};
// Run middleware pipeline
const middlewarePipeline = createMiddlewarePipeline(cfg, wahaClient, logger);
await middlewarePipeline(context);
// Check if middleware marked context to skip
if (context.skip) {
if (context.error) {
// Send error message if provided
if (context.error.type === 'AUTH_ERROR') {
await sendMessage(wahaClient, cfg, chatId, `❌ ${context.error.message}`);
}
}
return;
}
// Check if message text is empty after normalization
if (!context.messageText || !context.messageText.trim()) {
logger?.debug('Empty message text, ignoring', { chatId, messageId });
return;
}
// Find matching command
const commandRegistry = getCommandRegistry();
const commandMatch = commandRegistry.findCommand(context.messageText, context);
if (!commandMatch) {
// No command matched - ignore the message (this is expected for non-command messages)
logger?.debug('No command matched - message does not match any command pattern, ignoring', {
messageText: context.messageText?.substring(0, 100) || '(empty)',
chatId,
messageLength: context.messageText?.length || 0
});
return;
}
// Log matched command
const commandName = commandMatch.command.name;
logger?.info(`🎯 Processing command: ${commandName}`);
// Execute the matched command
await commandRegistry.executeCommand(
commandMatch.command,
commandMatch.matchResult,
context
);
} catch (err) {
logger?.error('Error handling message', {
...getErrorDetails(err, 'handleMessage'),
chatId: payload?.from,
messageId: payload?.id
});
}
}
/**
* Main function
*/
async function main() {
const cfg = loadConfig({ requireWaha: true, requireWebhook: true });
cfg.__logger = createLogger(cfg);
const logger = cfg.__logger;
// Initialize state manager, queue manager, and subscription manager
createStateManager(cfg);
createQueueManager(cfg);
createSubscriptionManager(logger, cfg);
const jellyseerrClient = createHttpClient(cfg.jellyseerr.apiBaseUrl);
const wahaClient = createWahaClient(cfg.waha.baseUrl);
logger?.info('🤖 Starting WhatsApp bot...');
logger?.info(`🔗 Jellyseerr: ${cfg.jellyseerr.baseUrl}`);
logger?.info(`🔗 WAHA: ${cfg.waha.baseUrl}`);
logger?.info(`🧩 WAHA Session: ${cfg.waha?.session || 'default'}`);
// Create and start Fastify server
const server = await createServer(cfg, jellyseerrClient, wahaClient, handleMessage);
// Graceful shutdown handler
const shutdown = async () => {
logger?.info('\n🛑 Shutting down…');
try {
await server.close();
const queueManager = getQueueManager();
queueManager.destroy(); // Clean up queue manager and timers
logger?.info('✅ Server closed.');
logger?.info('✅ Queue manager cleaned up.');
process.exit(0);
} catch (err) {
logger?.error('Error during shutdown', getErrorDetails(err, 'shutdown'));
process.exit(1);
}
};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);
}
// Version display
if (process.argv.includes('--version') || process.argv.includes('-v')) {
const fs = await import('fs/promises');
const path = await import('path');
const url = await import('url');
const __dirname = path.dirname(url.fileURLToPath(import.meta.url));
const pkg = JSON.parse(await fs.readFile(path.join(__dirname, 'package.json'), 'utf8'));
console.log(`Whatseerr v${pkg.version}`);
process.exit(0);
}
if (import.meta.url === `file://${process.argv[1]}`) {
main().catch((err) => {
const cfg = (() => {
try { return loadConfig({ requireWaha: false, requireWebhook: false }); } catch { return {}; }
})();
const logger = createLogger(cfg);
logger?.error('Fatal error', err?.message || err);
if (err?.stack) {
logger?.debug('Fatal stack', err.stack);
}
process.exit(1);
});
}