Merge branch 'main' of https://github.com/josedario87/conversation-layer
All checks were successful
Deploy conversation layer / deploy (push) Successful in 2m13s
All checks were successful
Deploy conversation layer / deploy (push) Successful in 2m13s
This commit is contained in:
@@ -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 {
|
||||
|
||||
90
whatsapp-router/src/messageProcessor.ts
Normal file
90
whatsapp-router/src/messageProcessor.ts
Normal 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
|
||||
};
|
||||
313
whatsapp-router/src/nucleoClient.ts
Normal file
313
whatsapp-router/src/nucleoClient.ts
Normal 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}`);
|
||||
}
|
||||
}
|
||||
263
whatsapp-router/src/routes/nucleoActions.ts
Normal file
263
whatsapp-router/src/routes/nucleoActions.ts
Normal 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;
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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 || '';
|
||||
|
||||
|
||||
message = await handleAudioMessageTranscription(message, openWaUrl);
|
||||
} 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.
|
||||
// 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 {
|
||||
|
||||
Reference in New Issue
Block a user