Merge branch 'main' of https://github.com/josedario87/conversation-layer
All checks were successful
Deploy conversation layer / deploy (push) Successful in 2m13s

This commit is contained in:
2025-06-06 21:43:31 -06:00
6 changed files with 702 additions and 70 deletions

View File

@@ -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 {

View File

@@ -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<WhatsAppMessage> => {
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
};

View File

@@ -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<T = any> {
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<OpenWAResponse> { // Using OpenWAResponse<any> 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<OpenWAResponse> {
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<OpenWAResponse> {
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<OpenWAResponse<Chat>> { // 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<OpenWAResponse> {
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<OpenWAResponse<Contact>> { // 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<OpenWAResponse<string[]>> { // 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<OpenWAResponse> {
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<OpenWAResponse> {
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<OpenWAResponse<Chat[]>> { // 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<OpenWAResponse<WhatsAppMessage[]>> { // 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}`);
}
}

View File

@@ -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;

View File

@@ -1,5 +1,6 @@
import axios from 'axios';
import { WhatsAppMessage, Conversation, Msg, Participant } from '../types';
import { mapWhatsAppMessageToMsg, processIncomingMessageForConversation } from '../messageProcessor';
const conversations = new Map<string, Conversation>();
@@ -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<Conversation> {
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<Conversation> {
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<Conversation> {
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;
}

View File

@@ -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,