This commit introduces a significant refactoring to the whatsapp-router service to improve modularity and extend its capabilities for interacting with OpenWA (nucleo-whatsapp).
Key changes include:
1. **Message Processing Logic**:
* Created a new `messageProcessor.ts` module.
* Moved message mapping (`mapWhatsAppMessageToMsg`), message addition to conversations (`processIncomingMessageForConversation`), and audio transcription handling (`handleAudioMessageTranscription`) into this new module.
* Updated `store/conversation.ts` and `webhook.ts` to utilize `messageProcessor.ts`, streamlining their responsibilities.
2. **OpenWA Client (`nucleoClient.ts`)**:
* Introduced `nucleoClient.ts`, a dedicated module for encapsulating API calls to OpenWA.
* Implemented functions for various OpenWA commands:
* Sending text, image, and file messages.
* Fetching chat and contact information.
* Creating groups.
* Managing the blocklist (get, block, unblock).
* Listing all chats and fetching messages for a specific chat.
* Includes error handling and basic typing for API responses.
3. **New API Endpoints (`routes/nucleoActions.ts`)**:
* Created `nucleoActions.ts` to expose the functionalities of `nucleoClient.ts` via a new set of HTTP API endpoints.
* Endpoints cover all implemented client functions, with request validation and robust error handling.
* These routes are grouped under the `/nucleo` base path.
4. **Route Registration**:
* Registered the new `/nucleo` routes in the main `index.ts` file.
5. **Conversation Store and Routes Review**:
* Reviewed and confirmed that `store/conversation.ts` and `routes/conversations.ts` are correctly integrated with the new `messageProcessor.ts` and remain focused on their core responsibilities.
This refactoring enhances the structure of `whatsapp-router`, making it easier to maintain and extend. It also provides a comprehensive set of API endpoints for more granular control over OpenWA functionalities.
264 lines
10 KiB
TypeScript
264 lines
10 KiB
TypeScript
import express, { Router, Request, Response, NextFunction } from 'express';
|
|
import * as nucleoClient from '../nucleoClient';
|
|
// Assuming OPEN_WA_URL is set in the environment.
|
|
// For local development, dotenv would typically be used in the main app entry point (e.g., index.ts)
|
|
// require('dotenv').config(); // Potentially, but better if handled by the main application loader
|
|
|
|
const router = Router();
|
|
|
|
// Retrieve OpenWA URL from environment variables
|
|
// This is done once when the module is loaded.
|
|
const openWaUrl = process.env.OPEN_WA_URL;
|
|
|
|
// Middleware to check if openWaUrl is configured
|
|
// This runs for every request to this router
|
|
router.use((req: Request, res: Response, next: NextFunction) => {
|
|
if (!openWaUrl) {
|
|
console.error('[routes/nucleoActions] Service OPEN_WA_URL not configured');
|
|
return res.status(500).json({ error: 'Service OPEN_WA_URL not configured. Please set the environment variable.' });
|
|
}
|
|
// Pass openWaUrl to subsequent handlers via res.locals if preferred,
|
|
// or they can access the `openWaUrl` constant from the module scope.
|
|
// For simplicity, handlers will use the module-scoped `openWaUrl`.
|
|
next();
|
|
});
|
|
|
|
// Route implementations
|
|
|
|
// POST /send-text
|
|
router.post('/send-text', async (req: Request, res: Response) => {
|
|
try {
|
|
const { to, content } = req.body;
|
|
if (!to || !content) {
|
|
return res.status(400).json({ error: 'Missing "to" or "content" in request body' });
|
|
}
|
|
// openWaUrl is checked by middleware and available in module scope
|
|
const result = await nucleoClient.sendTextMessage(openWaUrl!, to, content);
|
|
res.json(result);
|
|
} catch (error: any) {
|
|
console.error('[routes/nucleoActions] Error in /send-text:', error.message);
|
|
// Check if the error message is already a JSON string from nucleoClient's error handling
|
|
try {
|
|
const parsedError = JSON.parse(error.message.substring(error.message.indexOf('{')));
|
|
return res.status(parsedError.status || 500).json({ error: parsedError });
|
|
} catch (e) {
|
|
// If not, send the plain error message
|
|
res.status(500).json({ error: error.message || 'Failed to send text message' });
|
|
}
|
|
}
|
|
});
|
|
|
|
// POST /send-image
|
|
router.post('/send-image', async (req: Request, res: Response) => {
|
|
try {
|
|
const { to, path, caption } = req.body;
|
|
if (!to || !path) {
|
|
return res.status(400).json({ error: 'Missing "to" or "path" in request body' });
|
|
}
|
|
const result = await nucleoClient.sendImageMessage(openWaUrl!, to, path, caption);
|
|
res.json(result);
|
|
} catch (error: any) {
|
|
console.error('[routes/nucleoActions] Error in /send-image:', error.message);
|
|
try {
|
|
const parsedError = JSON.parse(error.message.substring(error.message.indexOf('{')));
|
|
return res.status(parsedError.status || 500).json({ error: parsedError });
|
|
} catch (e) {
|
|
res.status(500).json({ error: error.message || 'Failed to send image message' });
|
|
}
|
|
}
|
|
});
|
|
|
|
// POST /send-file
|
|
router.post('/send-file', async (req: Request, res: Response) => {
|
|
try {
|
|
const { to, path, filename, caption } = req.body;
|
|
if (!to || !path) {
|
|
return res.status(400).json({ error: 'Missing "to" or "path" in request body' });
|
|
}
|
|
const result = await nucleoClient.sendFileMessage(openWaUrl!, to, path, filename, caption);
|
|
res.json(result);
|
|
} catch (error: any) {
|
|
console.error('[routes/nucleoActions] Error in /send-file:', error.message);
|
|
try {
|
|
const parsedError = JSON.parse(error.message.substring(error.message.indexOf('{')));
|
|
return res.status(parsedError.status || 500).json({ error: parsedError });
|
|
} catch (e) {
|
|
res.status(500).json({ error: error.message || 'Failed to send file message' });
|
|
}
|
|
}
|
|
});
|
|
|
|
// GET /chats/:chatId
|
|
router.get('/chats/:chatId', async (req: Request, res: Response) => {
|
|
try {
|
|
const { chatId } = req.params;
|
|
if (!chatId) {
|
|
return res.status(400).json({ error: 'Missing "chatId" in request params' });
|
|
}
|
|
const result = await nucleoClient.getChatById(openWaUrl!, chatId);
|
|
res.json(result);
|
|
} catch (error: any) {
|
|
console.error(`[routes/nucleoActions] Error in /chats/${req.params.chatId}:`, error.message);
|
|
try {
|
|
const parsedError = JSON.parse(error.message.substring(error.message.indexOf('{')));
|
|
return res.status(parsedError.status || 500).json({ error: parsedError });
|
|
} catch (e) {
|
|
res.status(500).json({ error: error.message || 'Failed to retrieve chat' });
|
|
}
|
|
}
|
|
});
|
|
|
|
// POST /groups
|
|
router.post('/groups', async (req: Request, res: Response) => {
|
|
try {
|
|
const { groupName, contactIds } = req.body;
|
|
if (!groupName || !contactIds || !Array.isArray(contactIds) || contactIds.length === 0) {
|
|
return res.status(400).json({ error: 'Missing "groupName" or "contactIds" (must be a non-empty array) in request body' });
|
|
}
|
|
const result = await nucleoClient.createGroup(openWaUrl!, groupName, contactIds);
|
|
res.json(result);
|
|
} catch (error: any) {
|
|
console.error('[routes/nucleoActions] Error in /groups:', error.message);
|
|
try {
|
|
const parsedError = JSON.parse(error.message.substring(error.message.indexOf('{')));
|
|
return res.status(parsedError.status || 500).json({ error: parsedError });
|
|
} catch (e) {
|
|
res.status(500).json({ error: error.message || 'Failed to create group' });
|
|
}
|
|
}
|
|
});
|
|
|
|
// GET /contacts/:contactId
|
|
router.get('/contacts/:contactId', async (req: Request, res: Response) => {
|
|
try {
|
|
const { contactId } = req.params;
|
|
if (!contactId) {
|
|
return res.status(400).json({ error: 'Missing "contactId" in request params' });
|
|
}
|
|
const result = await nucleoClient.getContact(openWaUrl!, contactId);
|
|
res.json(result);
|
|
} catch (error: any) {
|
|
console.error(`[routes/nucleoActions] Error in /contacts/${req.params.contactId}:`, error.message);
|
|
try {
|
|
const parsedError = JSON.parse(error.message.substring(error.message.indexOf('{')));
|
|
return res.status(parsedError.status || 500).json({ error: parsedError });
|
|
} catch (e) {
|
|
res.status(500).json({ error: error.message || 'Failed to retrieve contact' });
|
|
}
|
|
}
|
|
});
|
|
|
|
// GET /blocklist
|
|
router.get('/blocklist', async (req: Request, res: Response) => {
|
|
try {
|
|
const result = await nucleoClient.getBlocklist(openWaUrl!);
|
|
res.json(result);
|
|
} catch (error: any) {
|
|
console.error('[routes/nucleoActions] Error in /blocklist:', error.message);
|
|
try {
|
|
const parsedError = JSON.parse(error.message.substring(error.message.indexOf('{')));
|
|
return res.status(parsedError.status || 500).json({ error: parsedError });
|
|
} catch (e) {
|
|
res.status(500).json({ error: error.message || 'Failed to retrieve blocklist' });
|
|
}
|
|
}
|
|
});
|
|
|
|
// POST /blocklist/block
|
|
router.post('/blocklist/block', async (req: Request, res: Response) => {
|
|
try {
|
|
const { contactId } = req.body;
|
|
if (!contactId) {
|
|
return res.status(400).json({ error: 'Missing "contactId" in request body' });
|
|
}
|
|
const result = await nucleoClient.blockContact(openWaUrl!, contactId);
|
|
res.json(result);
|
|
} catch (error: any) {
|
|
console.error('[routes/nucleoActions] Error in /blocklist/block:', error.message);
|
|
try {
|
|
const parsedError = JSON.parse(error.message.substring(error.message.indexOf('{')));
|
|
return res.status(parsedError.status || 500).json({ error: parsedError });
|
|
} catch (e) {
|
|
res.status(500).json({ error: error.message || 'Failed to block contact' });
|
|
}
|
|
}
|
|
});
|
|
|
|
// POST /blocklist/unblock
|
|
router.post('/blocklist/unblock', async (req: Request, res: Response) => {
|
|
try {
|
|
const { contactId } = req.body;
|
|
if (!contactId) {
|
|
return res.status(400).json({ error: 'Missing "contactId" in request body' });
|
|
}
|
|
const result = await nucleoClient.unblockContact(openWaUrl!, contactId);
|
|
res.json(result);
|
|
} catch (error: any) {
|
|
console.error('[routes/nucleoActions] Error in /blocklist/unblock:', error.message);
|
|
try {
|
|
const parsedError = JSON.parse(error.message.substring(error.message.indexOf('{')));
|
|
return res.status(parsedError.status || 500).json({ error: parsedError });
|
|
} catch (e) {
|
|
res.status(500).json({ error: error.message || 'Failed to unblock contact' });
|
|
}
|
|
}
|
|
});
|
|
|
|
// GET /chats
|
|
router.get('/chats', async (req: Request, res: Response) => {
|
|
try {
|
|
const result = await nucleoClient.getAllChats(openWaUrl!);
|
|
res.json(result);
|
|
} catch (error: any) {
|
|
console.error('[routes/nucleoActions] Error in /chats:', error.message);
|
|
try {
|
|
const parsedError = JSON.parse(error.message.substring(error.message.indexOf('{')));
|
|
return res.status(parsedError.status || 500).json({ error: parsedError });
|
|
} catch (e) {
|
|
res.status(500).json({ error: error.message || 'Failed to retrieve all chats' });
|
|
}
|
|
}
|
|
});
|
|
|
|
// GET /chats/:chatId/messages
|
|
router.get('/chats/:chatId/messages', async (req: Request, res: Response) => {
|
|
try {
|
|
const { chatId } = req.params;
|
|
if (!chatId) {
|
|
return res.status(400).json({ error: 'Missing "chatId" in request params' });
|
|
}
|
|
|
|
const { limit, includeMe, includeNotifications } = req.query;
|
|
|
|
let numLimit: number | undefined = undefined;
|
|
if (limit) {
|
|
numLimit = parseInt(limit as string, 10);
|
|
if (isNaN(numLimit)) {
|
|
return res.status(400).json({ error: 'Invalid "limit" query parameter, must be a number.' });
|
|
}
|
|
}
|
|
|
|
const boolIncludeMe: boolean | undefined = includeMe ? (includeMe as string).toLowerCase() === 'true' : undefined;
|
|
const boolIncludeNotifications: boolean | undefined = includeNotifications ? (includeNotifications as string).toLowerCase() === 'true' : undefined;
|
|
|
|
const result = await nucleoClient.getChatMessages(
|
|
openWaUrl!,
|
|
chatId,
|
|
numLimit,
|
|
boolIncludeMe,
|
|
boolIncludeNotifications
|
|
);
|
|
res.json(result);
|
|
} catch (error: any) {
|
|
console.error(`[routes/nucleoActions] Error in /chats/${req.params.chatId}/messages:`, error.message);
|
|
try {
|
|
const parsedError = JSON.parse(error.message.substring(error.message.indexOf('{')));
|
|
return res.status(parsedError.status || 500).json({ error: parsedError });
|
|
} catch (e) {
|
|
res.status(500).json({ error: error.message || 'Failed to retrieve chat messages' });
|
|
}
|
|
}
|
|
});
|
|
|
|
export default router;
|