diff --git a/whatsapp-router/src/index.ts b/whatsapp-router/src/index.ts index 3917582..b4314b6 100644 --- a/whatsapp-router/src/index.ts +++ b/whatsapp-router/src/index.ts @@ -8,6 +8,7 @@ import { waitForGateway, WebhookConfig, } from './webhook'; +import nucleoActionsRouter from './routes/nucleoActions'; // New import dotenv.config(); @@ -51,6 +52,9 @@ const config: WebhookConfig = { registerConversationRoutes(app, openWaUrl); registerWebhookRoutes(app, config, openWaUrl, agentUrl); +// Register new nucleoActions routes +app.use('/nucleo', nucleoActionsRouter); // New line + app.listen(port, async () => { console.log(`WhatsApp router listening on ${port}`); try { diff --git a/whatsapp-router/src/messageProcessor.ts b/whatsapp-router/src/messageProcessor.ts new file mode 100644 index 0000000..47635e3 --- /dev/null +++ b/whatsapp-router/src/messageProcessor.ts @@ -0,0 +1,90 @@ +import { WhatsAppMessage, Msg, Conversation } from "./types"; +import { transcribeAudioMessage } from "./transcribeAudioMessage"; +import axios from 'axios'; // Needed for sending error messages + +export const mapWhatsAppMessageToMsg = (m: WhatsAppMessage): Msg => { + return { + id: m.id, + from: m.from, + to: m.to, + ts: (m as any).timestamp || (m as any).t, + type: ((m as any).type as any) || 'chat', + text: (m as any).text || (m as any).caption || (m as any).body, + mediaUrl: (m as any).cloudUrl || (m as any).clientUrl, + mentions: ((m as any).mentionedJidList as any) || [], + meta: { + ack: (m as any).ack || 0, + hasReaction: (m as any).hasReaction || false, + isQuoted: !!(m as any).quotedMsg, + }, + }; +}; + +// processIncomingMessageForConversation incorporates logic from former addMessageToConversation +export const processIncomingMessageForConversation = ( + conversation: Conversation, + message: WhatsAppMessage +): void => { + const mappedMsg = mapWhatsAppMessageToMsg(message); + + // Avoid duplicates if multiple webhook events deliver the same message + if (!conversation.messages.some((m) => m.id === mappedMsg.id)) { + conversation.messages.push(mappedMsg); + // if (conversation.messages.length > 20) { + // conversation.messages.shift(); // Keep only the last 20 messages + // } + } + + // Add new participants if necessary + const sender = message.sender; + if (sender && !conversation.participants.some((p) => p.id === sender.id)) { + conversation.participants.push({ + id: sender.id, + name: sender.pushname || sender.name || '', + isMe: sender.isMe, + // isAdmin property is not available here, it's part of group metadata + }); + } + // The conversation object is modified directly +}; + +export const handleAudioMessageTranscription = async ( + message: WhatsAppMessage, + openWaUrl: string // Needed to send a reply if transcription fails +): Promise => { + if ( + message.type === 'ptt' && + message.mimetype === 'audio/ogg; codecs=opus' + ) { + const audioUrl = message.clientUrl || message.deprecatedMms3Url; + if (!audioUrl) { + console.error('[MessageProcessor] No audio URL found for PTT message'); + throw new Error('No audio URL found for PTT message'); + } + try { + const transcript = await transcribeAudioMessage(message); + console.log('[MessageProcessor] 📝 Transcription:', transcript); + message.body = transcript || ''; // Update message body with transcript + message.text = transcript || ''; // Also update text property + } catch (transcriptionError: any) { + console.error('[MessageProcessor] Error in transcription:', transcriptionError.message); + const reply = + "I received an audio message, but I couldn't transcribe it. Please send the transcript manually. ⚠️⚠️"; + // Need from/chatId to send reply + const chatId = message.chatId || message.from; + if (chatId && openWaUrl) { + try { + await axios.post(`${openWaUrl}/sendText`, { args: { to: chatId, content: reply } }); + } catch (axiosError) { + console.error('[MessageProcessor] Failed to send transcription error message via Axios:', axiosError); + } + } + // We might want to throw an error here to stop further processing in webhook.ts + // For now, returning the original message, but webhook should check if body is empty. + // Or, perhaps, it's better to let webhook decide to stop processing. + // Let's re-throw to allow webhook to catch and decide. + throw transcriptionError; + } + } + return message; // Return original or modified message +}; diff --git a/whatsapp-router/src/nucleoClient.ts b/whatsapp-router/src/nucleoClient.ts new file mode 100644 index 0000000..da7eae9 --- /dev/null +++ b/whatsapp-router/src/nucleoClient.ts @@ -0,0 +1,313 @@ +import axios from 'axios'; +import { Chat, Contact, WhatsAppMessage } from './types'; // Corrected to WhatsAppMessage + +// Placeholder for a generic OpenWA API response structure. +// This can be refined later if specific response types are identified. +export interface OpenWAResponse { + success: boolean; + response?: T; // Often the actual data is nested here + data?: T; // Sometimes it might be here + message?: string; + // Other common fields can be added if observed from OpenWA responses +} + +/** + * Sends a text message via OpenWA. + * @param openWaUrl The base URL of the OpenWA instance. + * @param to The recipient's chat ID (e.g., 'xxxxxxxxxxx@c.us'). + * @param content The text message content. + * @returns A promise that resolves to the API response. + */ +export async function sendTextMessage( + openWaUrl: string, + to: string, + content: string +): Promise { // Using OpenWAResponse for now + try { + const response = await axios.post(`${openWaUrl}/sendText`, { + args: { to, content }, + }); + // The actual data might be in response.data.response or response.data + // This needs to be consistent with how OpenWA structures its responses. + // Prioritizing response.data.response if it exists. + return response.data?.response || response.data; + } catch (error: any) { + console.error(`[nucleoClient] Error sending text message to ${to}:`, error.message); + if (axios.isAxiosError(error) && error.response) { + // Log more detailed error from OpenWA if available + console.error('[nucleoClient] Axios error details:', error.response.data); + throw new Error(`nucleoClient API error (${openWaUrl}/sendText): ${error.response.status} - ${JSON.stringify(error.response.data)}`); + } + throw new Error(`nucleoClient error (${openWaUrl}/sendText): ${error.message}`); + } +} + +/** + * Sends an image message via OpenWA. + * @param openWaUrl The base URL of the OpenWA instance. + * @param to The recipient's chat ID. + * @param path URL or local path of the image OpenWA can access. + * @param caption Optional caption for the image. + * @returns A promise that resolves to the API response. + */ +export async function sendImageMessage( + openWaUrl: string, + to: string, + path: string, // Assuming 'path' is how OpenWA refers to the image URL/path + caption?: string +): Promise { + try { + const args: { to: string; path: string; caption?: string } = { to, path }; + if (caption) { + args.caption = caption; + } + const response = await axios.post(`${openWaUrl}/sendImage`, { args }); + return response.data?.response || response.data; + } catch (error: any) { + console.error(`[nucleoClient] Error sending image message to ${to}:`, error.message); + if (axios.isAxiosError(error) && error.response) { + console.error('[nucleoClient] Axios error details:', error.response.data); + throw new Error(`nucleoClient API error (${openWaUrl}/sendImage): ${error.response.status} - ${JSON.stringify(error.response.data)}`); + } + throw new Error(`nucleoClient error (${openWaUrl}/sendImage): ${error.message}`); + } +} + +/** + * Sends a file/document message via OpenWA. + * @param openWaUrl The base URL of the OpenWA instance. + * @param to The recipient's chat ID. + * @param path URL or local path of the file OpenWA can access. + * @param filename Optional name for the file. + * @param caption Optional caption for the file. + * @returns A promise that resolves to the API response. + */ +export async function sendFileMessage( + openWaUrl: string, + to: string, + path: string, // Assuming 'path' is how OpenWA refers to the file URL/path + filename?: string, + caption?: string +): Promise { + try { + const args: { to: string; path: string; filename?: string; caption?: string } = { to, path }; + if (filename) { + args.filename = filename; + } + if (caption) { + args.caption = caption; + } + const response = await axios.post(`${openWaUrl}/sendFile`, { args }); + return response.data?.response || response.data; + } catch (error: any) { + console.error(`[nucleoClient] Error sending file message to ${to}:`, error.message); + if (axios.isAxiosError(error) && error.response) { + console.error('[nucleoClient] Axios error details:', error.response.data); + throw new Error(`nucleoClient API error (${openWaUrl}/sendFile): ${error.response.status} - ${JSON.stringify(error.response.data)}`); + } + throw new Error(`nucleoClient error (${openWaUrl}/sendFile): ${error.message}`); + } +} + +/** + * Retrieves chat details by chat ID. + * @param openWaUrl The base URL of the OpenWA instance. + * @param chatId The ID of the chat to retrieve. + * @returns A promise that resolves to the API response containing chat details. + */ +export async function getChatById( + openWaUrl: string, + chatId: string +): Promise> { // Assuming Chat type from types.ts + try { + const response = await axios.post(`${openWaUrl}/getChatById`, { args: { chatId } }); + return response.data?.response || response.data; + } catch (error: any) { + console.error(`[nucleoClient] Error retrieving chat ${chatId}:`, error.message); + if (axios.isAxiosError(error) && error.response) { + console.error('[nucleoClient] Axios error details:', error.response.data); + throw new Error(`nucleoClient API error (${openWaUrl}/getChatById): ${error.response.status} - ${JSON.stringify(error.response.data)}`); + } + throw new Error(`nucleoClient error (${openWaUrl}/getChatById): ${error.message}`); + } +} + +/** + * Creates a new group. + * @param openWaUrl The base URL of the OpenWA instance. + * @param groupName The name of the group to create. + * @param contactIds An array of contact IDs to add to the group. + * @returns A promise that resolves to the API response. + */ +export async function createGroup( + openWaUrl: string, + groupName: string, + contactIds: string[] +): Promise { + try { + const response = await axios.post(`${openWaUrl}/createGroup`, { + args: { groupName, contactIds }, + }); + return response.data?.response || response.data; + } catch (error: any) { + console.error(`[nucleoClient] Error creating group "${groupName}":`, error.message); + if (axios.isAxiosError(error) && error.response) { + console.error('[nucleoClient] Axios error details:', error.response.data); + throw new Error(`nucleoClient API error (${openWaUrl}/createGroup): ${error.response.status} - ${JSON.stringify(error.response.data)}`); + } + throw new Error(`nucleoClient error (${openWaUrl}/createGroup): ${error.message}`); + } +} + +/** + * Retrieves contact details by contact ID. + * @param openWaUrl The base URL of the OpenWA instance. + * @param contactId The ID of the contact to retrieve. + * @returns A promise that resolves to the API response containing contact details. + */ +export async function getContact( + openWaUrl: string, + contactId: string +): Promise> { // Assuming Contact type from types.ts + try { + const response = await axios.post(`${openWaUrl}/getContact`, { args: { contactId } }); + return response.data?.response || response.data; + } catch (error: any) { + console.error(`[nucleoClient] Error retrieving contact ${contactId}:`, error.message); + if (axios.isAxiosError(error) && error.response) { + console.error('[nucleoClient] Axios error details:', error.response.data); + throw new Error(`nucleoClient API error (${openWaUrl}/getContact): ${error.response.status} - ${JSON.stringify(error.response.data)}`); + } + throw new Error(`nucleoClient error (${openWaUrl}/getContact): ${error.message}`); + } +} + +/** + * Retrieves the blocklist. + * @param openWaUrl The base URL of the OpenWA instance. + * @returns A promise that resolves to the API response containing the blocklist. + */ +export async function getBlocklist( + openWaUrl: string +): Promise> { // Assuming blocklist is an array of contact IDs + try { + // Some GET operations might be POST in OpenWA, or might not need args + const response = await axios.post(`${openWaUrl}/getBlocklist`); + return response.data?.response || response.data; + } catch (error: any) { + console.error(`[nucleoClient] Error retrieving blocklist:`, error.message); + if (axios.isAxiosError(error) && error.response) { + console.error('[nucleoClient] Axios error details:', error.response.data); + throw new Error(`nucleoClient API error (${openWaUrl}/getBlocklist): ${error.response.status} - ${JSON.stringify(error.response.data)}`); + } + throw new Error(`nucleoClient error (${openWaUrl}/getBlocklist): ${error.message}`); + } +} + +/** + * Blocks a contact. + * @param openWaUrl The base URL of the OpenWA instance. + * @param contactId The ID of the contact to block. + * @returns A promise that resolves to the API response. + */ +export async function blockContact( + openWaUrl: string, + contactId: string +): Promise { + try { + const response = await axios.post(`${openWaUrl}/blockContact`, { args: { contactId } }); + return response.data?.response || response.data; + } catch (error: any) { + console.error(`[nucleoClient] Error blocking contact ${contactId}:`, error.message); + if (axios.isAxiosError(error) && error.response) { + console.error('[nucleoClient] Axios error details:', error.response.data); + throw new Error(`nucleoClient API error (${openWaUrl}/blockContact): ${error.response.status} - ${JSON.stringify(error.response.data)}`); + } + throw new Error(`nucleoClient error (${openWaUrl}/blockContact): ${error.message}`); + } +} + +/** + * Unblocks a contact. + * @param openWaUrl The base URL of the OpenWA instance. + * @param contactId The ID of the contact to unblock. + * @returns A promise that resolves to the API response. + */ +export async function unblockContact( + openWaUrl: string, + contactId: string +): Promise { + try { + const response = await axios.post(`${openWaUrl}/unblockContact`, { args: { contactId } }); + return response.data?.response || response.data; + } catch (error: any) { + console.error(`[nucleoClient] Error unblocking contact ${contactId}:`, error.message); + if (axios.isAxiosError(error) && error.response) { + console.error('[nucleoClient] Axios error details:', error.response.data); + throw new Error(`nucleoClient API error (${openWaUrl}/unblockContact): ${error.response.status} - ${JSON.stringify(error.response.data)}`); + } + throw new Error(`nucleoClient error (${openWaUrl}/unblockContact): ${error.message}`); + } +} + +/** + * Retrieves all chats. + * @param openWaUrl The base URL of the OpenWA instance. + * @returns A promise that resolves to the API response containing all chats. + */ +export async function getAllChats( + openWaUrl: string +): Promise> { // Assuming an array of Chat objects + try { + // This might be a GET request or a POST without args, depending on OpenWA + const response = await axios.post(`${openWaUrl}/getAllChats`); + return response.data?.response || response.data; + } catch (error: any) { + console.error(`[nucleoClient] Error retrieving all chats:`, error.message); + if (axios.isAxiosError(error) && error.response) { + console.error('[nucleoClient] Axios error details:', error.response.data); + throw new Error(`nucleoClient API error (${openWaUrl}/getAllChats): ${error.response.status} - ${JSON.stringify(error.response.data)}`); + } + throw new Error(`nucleoClient error (${openWaUrl}/getAllChats): ${error.message}`); + } +} + +/** + * Retrieves messages for a specific chat. + * Maps to OpenWA's /loadAndGetAllMessagesInChat endpoint. + * @param openWaUrl The base URL of the OpenWA instance. + * @param chatId The ID of the chat. + * @param limit Optional limit for the number of messages. + * @param includeMe Optional flag to include messages sent by oneself. + * @param includeNotifications Optional flag to include notification messages. + * @returns A promise that resolves to the API response containing messages. + */ +export async function getChatMessages( + openWaUrl: string, + chatId: string, + limit?: number, + includeMe?: boolean, + includeNotifications?: boolean +): Promise> { // Corrected to WhatsAppMessage[] + try { + const args: { + chatId: string; + limit?: number; + includeMe?: boolean; + includeNotifications?: boolean; + } = { chatId }; + if (limit !== undefined) args.limit = limit; + if (includeMe !== undefined) args.includeMe = includeMe; + if (includeNotifications !== undefined) args.includeNotifications = includeNotifications; + + const response = await axios.post(`${openWaUrl}/loadAndGetAllMessagesInChat`, { args }); + return response.data?.response || response.data; + } catch (error: any) { + console.error(`[nucleoClient] Error retrieving messages for chat ${chatId}:`, error.message); + if (axios.isAxiosError(error) && error.response) { + console.error('[nucleoClient] Axios error details:', error.response.data); + throw new Error(`nucleoClient API error (${openWaUrl}/loadAndGetAllMessagesInChat): ${error.response.status} - ${JSON.stringify(error.response.data)}`); + } + throw new Error(`nucleoClient error (${openWaUrl}/loadAndGetAllMessagesInChat): ${error.message}`); + } +} diff --git a/whatsapp-router/src/routes/nucleoActions.ts b/whatsapp-router/src/routes/nucleoActions.ts new file mode 100644 index 0000000..4c3c585 --- /dev/null +++ b/whatsapp-router/src/routes/nucleoActions.ts @@ -0,0 +1,263 @@ +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; diff --git a/whatsapp-router/src/store/conversation.ts b/whatsapp-router/src/store/conversation.ts index a517459..6d494b3 100644 --- a/whatsapp-router/src/store/conversation.ts +++ b/whatsapp-router/src/store/conversation.ts @@ -1,5 +1,6 @@ import axios from 'axios'; import { WhatsAppMessage, Conversation, Msg, Participant } from '../types'; +import { mapWhatsAppMessageToMsg, processIncomingMessageForConversation } from '../messageProcessor'; const conversations = new Map(); @@ -19,32 +20,15 @@ async function loadMessages( return msgs; } -function mapMessage(m: WhatsAppMessage): Msg { - return { - id: m.id, - from: m.from, - to: m.to, - ts: (m as any).timestamp || (m as any).t, - type: ((m as any).type as any) || 'chat', - text: (m as any).text || (m as any).caption || (m as any).body, - mediaUrl: (m as any).cloudUrl || (m as any).clientUrl, - mentions: ((m as any).mentionedJidList as any) || [], - meta: { - ack: (m as any).ack || 0, - hasReaction: (m as any).hasReaction || false, - isQuoted: !!(m as any).quotedMsg, - }, - }; -} - export async function getConversation( chatId: string, - openWaUrl: string + openWaUrl: string, + incommingMessage?: WhatsAppMessage ): Promise { console.log(`[conversationStore] Retrieving conversation for ${chatId}`); let conv = conversations.get(chatId); if (!conv) { - conv = await buildConversation(chatId, openWaUrl); + conv = await buildConversation(chatId, openWaUrl, incommingMessage); } return conv; } @@ -56,13 +40,14 @@ export function listConversations(): Conversation[] { export async function buildConversation( chatId: string, - openWaUrl: string + openWaUrl: string, + incommingMessage?: WhatsAppMessage ): Promise { console.log(`[conversationStore] Building conversation for ${chatId}`); const rawMessages = await loadMessages(chatId, openWaUrl); const now = Date.now(); - const first = rawMessages[0]; + const first = rawMessages[0] = incommingMessage || rawMessages[0]; const chat = first?.chat; const title = chat?.formattedTitle || chat?.name || chatId; const isGroup = chat?.isGroup || false; @@ -101,7 +86,7 @@ export async function buildConversation( } } - const messages: Msg[] = rawMessages.slice(-20).map(mapMessage); + const messages: Msg[] = rawMessages.map(mapWhatsAppMessageToMsg); messages.sort((a, b) => a.ts - b.ts); const conv: Conversation = { @@ -129,21 +114,15 @@ export async function addMessageToConversation( openWaUrl: string ): Promise { console.log(`[conversationStore] Adding message to ${chatId}`); - const conv = await getConversation(chatId, openWaUrl); - const mapped = mapMessage(msg); - // avoid duplicates if multiple webhook events deliver the same message - if (!conv.messages.some((m) => m.id === mapped.id)) { - conv.messages.push(mapped); - if (conv.messages.length > 20) conv.messages.shift(); - } - const s = msg.sender; - if (s && !conv.participants.some((p) => p.id === s.id)) { - conv.participants.push({ - id: s.id, - name: s.pushname || s.name || '', - isMe: s.isMe, - }); - } + const conv = await getConversation(chatId, openWaUrl, msg); + // Delegate message processing to the new function + // processIncomingMessageForConversation modifies `conv` directly + processIncomingMessageForConversation(conv, msg); + + // Ensure the conversation is updated in the map (though it's by reference as conv is an object) + + conversations.set(chatId, conv); + return conv; } \ No newline at end of file diff --git a/whatsapp-router/src/webhook.ts b/whatsapp-router/src/webhook.ts index 75ad881..4449a78 100644 --- a/whatsapp-router/src/webhook.ts +++ b/whatsapp-router/src/webhook.ts @@ -1,10 +1,11 @@ import express, { Application } from 'express'; import axios from 'axios'; -import { GoogleGenAI } from '@google/genai'; +// import { GoogleGenAI } from '@google/genai'; // Unused after transcription logic move import { getHandler } from './chatHandlers'; import { addMessageToConversation } from './store/conversation'; import { WhatsAppMessage, Conversation } from './types'; -import { transcribeAudioMessage } from './transcribeAudioMessage'; +// import { transcribeAudioMessage } from './transcribeAudioMessage'; // Moved to messageProcessor +import { handleAudioMessageTranscription } from './messageProcessor'; // Added import export interface WebhookConfig { API_URL: string; @@ -48,41 +49,21 @@ export function registerWebhookRoutes( const chatId = message.chatId || from; // Audio message handling - // console.log(message); - - if ( - message.type === 'ptt' && - message.mimetype === 'audio/ogg; codecs=opus' - ) { - const audioUrl = message.clientUrl || message.deprecatedMms3Url; - if (!audioUrl) { - console.error('No audio URL found for PTT message'); - // Potentially send a message to user or just skip? For now, skip. - return res.sendStatus(200); - } - console.log('🎤 Mensaje de audio detectado', audioUrl); - try { - const transcript = await transcribeAudioMessage(message); - console.log('📝 Transcripción:', transcript); - message.body = transcript || ''; - message.text = transcript || ''; - - - } catch (transcriptionError: any) { - console.error('Error en la transcripción:', transcriptionError.message); - const reply = - "I received an audio message, but I couldn't transcribe it. Please send the transcript manually."; - await axios.post(`${openWaUrl}/sendText`, { args: { to: from, content: reply } }); - // Stop processing this message as transcription failed and user has been notified. - return res.sendStatus(200); - } + try { + message = await handleAudioMessageTranscription(message, openWaUrl); + } catch (transcriptionError: any) { + // Log the error already handled by handleAudioMessageTranscription (which also sends a message to user) + console.error('[Webhook] Transcription failed, stopping further processing for this message.', transcriptionError.message); + // Stop processing this message as transcription failed and user has been notified by the processor. + return res.sendStatus(200); } - console.log(message); let conv: Conversation | undefined; if (chatId) { try { conv = await addMessageToConversation(chatId, message, openWaUrl); + console.log(`🔄 Updated conversation for ${chatId}`, conv); + } catch (err: any) { console.warn('Failed updating conversation:', err.message); } @@ -92,6 +73,8 @@ export function registerWebhookRoutes( if (!handler) throw new Error('No handler configured'); let reply: string; if (typeof handler === 'string') { + console.log(`🔗 Calling agent at ${handler} for conversation ${chatId}\n`); + const agentRes = await axios.post(handler, { conversation: conv }); reply = agentRes.data.reply || agentRes.data; } else { @@ -170,7 +153,7 @@ export async function registerWebhook(config: WebhookConfig, port: number) { onChatDeleted: true, onChatOpened: true, onChatState: true, - onContactAdded: true, + onContactAdded: true, onGlobalParticipantsChanged: true, onGroupApprovalRequest: true, onGroupChange: true,