This commit modifies the audio download process in `whatsapp-router/src/webhook.ts` to align with the updated nucleo-whatsapp API. Previously, audio files were downloaded using a direct GET request to the file URL. This has been changed to a POST request to the `/downloadFileWithCredentials` endpoint provided by nucleo-whatsapp. Key changes: - Audio files are now downloaded by POSTing to `[OPEN_WA_URL]/downloadFileWithCredentials` with the audio file's URL in the request body. - The `/downloadFileWithCredentials` endpoint returns a base64 encoded string directly, so the explicit base64 conversion step after downloading has been removed. This change ensures compatibility with the correct API for fetching message media.
214 lines
7.3 KiB
TypeScript
214 lines
7.3 KiB
TypeScript
import express, { Application } from 'express';
|
|
import axios from 'axios';
|
|
import { GoogleGenAI } from '@google/genai';
|
|
import { getHandler } from './chatHandlers';
|
|
import { addMessageToConversation } from './store/conversation';
|
|
import { WhatsAppMessage, Conversation } from './types';
|
|
|
|
export interface WebhookConfig {
|
|
API_URL: string;
|
|
MAX_ATTEMPTS: number;
|
|
RETRY_MS: number;
|
|
}
|
|
|
|
export function registerWebhookRoutes(
|
|
app: Application,
|
|
config: WebhookConfig,
|
|
openWaUrl: string | undefined,
|
|
agentUrl: string | undefined
|
|
) {
|
|
app.post('/webhook', async (req: express.Request, res: express.Response) => {
|
|
let message: WhatsAppMessage | undefined;
|
|
let from: string | undefined;
|
|
|
|
try {
|
|
if (req.body && req.body.data) {
|
|
message = req.body.data as WhatsAppMessage;
|
|
from = req.body.data.from;
|
|
} else {
|
|
throw new Error('Invalid webhook format');
|
|
}
|
|
} catch {}
|
|
|
|
if (message) {
|
|
const origen = from || message.chatId || 'desconocido';
|
|
console.log(`📩 Mensaje recibido (${message.text}) de ${origen}`);
|
|
}
|
|
|
|
try {
|
|
if (!message) return res.sendStatus(200);
|
|
if (!openWaUrl) throw new Error('Service URLs not configured');
|
|
const chatId = message.chatId || from;
|
|
|
|
// Audio message handling
|
|
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 {
|
|
// Download audio using the /downloadFileWithCredentials endpoint
|
|
const audioResponse = await axios.post(`${openWaUrl}/downloadFileWithCredentials`, {
|
|
args: { url: audioUrl },
|
|
});
|
|
const audioBase64 = audioResponse.data; // This is already a base64 string
|
|
|
|
const apiKey = process.env.GOOGLE_API_KEY;
|
|
if (!apiKey) {
|
|
throw new Error('GOOGLE_API_KEY is not set');
|
|
}
|
|
const genAI = new GoogleGenAI({ apiKey });
|
|
|
|
// Corrected Gemini API call structure
|
|
const result = await genAI.models.generateContent({
|
|
model: 'gemini-pro', // Ensure this model supports inline audio or use appropriate one
|
|
contents: [
|
|
{ inlineData: { mimeType: 'audio/ogg', data: audioBase64 } },
|
|
{ text: 'Generate a transcript of the speech.' },
|
|
],
|
|
});
|
|
// result directly is GenerateContentResponse
|
|
const transcript = result.text; // Use the getter for text
|
|
if (transcript === undefined) {
|
|
throw new Error('Transcription resulted in undefined text.');
|
|
}
|
|
console.log('📝 Transcripción:', transcript);
|
|
message.body = 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);
|
|
}
|
|
}
|
|
|
|
let conv: Conversation | undefined;
|
|
if (chatId) {
|
|
try {
|
|
conv = await addMessageToConversation(chatId, message, openWaUrl);
|
|
} catch (err: any) {
|
|
console.warn('Failed updating conversation:', err.message);
|
|
}
|
|
}
|
|
if (!conv) throw new Error('Conversation unavailable');
|
|
const handler = getHandler(chatId, agentUrl);
|
|
if (!handler) throw new Error('No handler configured');
|
|
let reply: string;
|
|
if (typeof handler === 'string') {
|
|
const agentRes = await axios.post(handler, { conversation: conv });
|
|
reply = agentRes.data.reply || agentRes.data;
|
|
} else {
|
|
reply = await handler(conv);
|
|
}
|
|
await axios.post(`${openWaUrl}/sendText`, { args: { to: from, content: reply } });
|
|
} catch (err: any) {
|
|
console.error('Error processing message', err.message);
|
|
}
|
|
|
|
res.sendStatus(200);
|
|
});
|
|
}
|
|
|
|
export async function waitForGateway(config: WebhookConfig) {
|
|
for (let i = 1; i <= config.MAX_ATTEMPTS; i++) {
|
|
try {
|
|
await axios.get(`${config.API_URL}/api-docs/`);
|
|
console.log('🟢 nucleo-whatsapp ready');
|
|
return;
|
|
} catch (e) {
|
|
console.warn(
|
|
'Gateway not responding',
|
|
`connecting to: ${config.API_URL}/api-docs/ (attempt ${i}/${config.MAX_ATTEMPTS})…`,
|
|
e
|
|
);
|
|
await new Promise((r) => setTimeout(r, config.RETRY_MS));
|
|
}
|
|
}
|
|
throw new Error('nucleo-whatsapp did not respond in time');
|
|
}
|
|
|
|
export async function clearWebhooks(config: WebhookConfig) {
|
|
try {
|
|
const { data } = await axios.post(`${config.API_URL}/listWebhooks`);
|
|
const hooks = data?.response || [];
|
|
if (!hooks.length) {
|
|
console.log('No existing webhooks to remove');
|
|
return;
|
|
}
|
|
|
|
console.log(`Removing ${hooks.length} webhooks…`);
|
|
const results = await Promise.allSettled(
|
|
hooks.map((h: any) => axios.post(`${config.API_URL}/removeWebhook`, { args: { webhookId: h.id } }))
|
|
);
|
|
|
|
results.forEach((r: PromiseSettledResult<any>, i: number) => {
|
|
const id = hooks[i].id;
|
|
if (r.status === 'fulfilled' && (r as PromiseFulfilledResult<any>).value?.data?.response === true) {
|
|
console.log(`✔️ Removed webhook ${id}`);
|
|
} else {
|
|
console.warn(`⚠️ Failed to remove webhook ${id}`);
|
|
}
|
|
});
|
|
|
|
const ok = results.filter(
|
|
(r: PromiseSettledResult<any>) => r.status === 'fulfilled' && (r as PromiseFulfilledResult<any>).value?.data?.response === true
|
|
).length;
|
|
console.log(`Cleanup OK (${ok}/${hooks.length} removed)`);
|
|
} catch (e: any) {
|
|
console.error('Failed cleaning webhooks:', e.response?.data || e.message);
|
|
}
|
|
}
|
|
|
|
export async function registerWebhook(config: WebhookConfig, port: number) {
|
|
const url = process.env.WEBHOOK_URL || `http://whatsapp-router:${port}/webhook`;
|
|
const eventConfig = {
|
|
onAck: false,
|
|
onAddedToGroup: true,
|
|
// use onAnyMessage to also receive messages sent by nucleo-whatsapp itself
|
|
onAnyMessage: true,
|
|
onBattery: true,
|
|
onBroadcast: true,
|
|
onButton: true,
|
|
onCallState: false,
|
|
onChatDeleted: true,
|
|
onChatOpened: true,
|
|
onChatState: true,
|
|
onContactAdded: true,
|
|
onGlobalParticipantsChanged: true,
|
|
onGroupApprovalRequest: true,
|
|
onGroupChange: true,
|
|
onIncomingCall: false,
|
|
onLabel: true,
|
|
onLogout: true,
|
|
// disable onMessage to avoid duplicates with onAnyMessage
|
|
onMessage: false,
|
|
onMessageDeleted: true,
|
|
onNewProduct: true,
|
|
onOrder: true,
|
|
onPlugged: false,
|
|
onPollVote: true,
|
|
onReaction: true,
|
|
onRemovedFromGroup: false,
|
|
onStateChanged: false,
|
|
onStory: false,
|
|
} as const;
|
|
|
|
const events = Object.entries(eventConfig)
|
|
.filter(([_, enabled]) => enabled)
|
|
.map(([event]) => event);
|
|
|
|
const { data } = await axios.post(`${config.API_URL}/registerWebhook`, {
|
|
args: { url, events },
|
|
});
|
|
|
|
console.log('✔️ Webhook registered:', data);
|
|
}
|