Files
conversation-layer/whatsapp-router/src/routes/nucleoActions.ts
google-labs-jules[bot] 2ea837c2ae Refactor: Modularize whatsapp-router interactions
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.
2025-06-07 02:49:50 +00:00

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;