import axios from 'axios'; import { Readable } from 'stream'; export function normalizeCommand(text: string): string { return text .trim() .toLowerCase() .replace(/[.,!?;:]+$/, "") // Remove trailing punctuation .toUpperCase(); } export function detectIntent(text: string): 'YES' | 'NO' | 'UNKNOWN' { const normalized = text.trim().toLowerCase().replace(/[.,!?;:]+$/, ""); const yesWords = ['oui', 'ouais', 'wi', 'waaw', 'yes', 'yep', 'ok', 'd’accord', 'daccord', 'da’accord']; const noWords = ['non', 'déet', 'deet', 'no', 'nah', 'nein']; if (yesWords.some(w => normalized.includes(w))) return 'YES'; if (noWords.some(w => normalized.includes(w))) return 'NO'; return 'UNKNOWN'; } export function levenshteinDistance(a: string, b: string): number { const matrix: number[][] = []; for (let i = 0; i <= b.length; i++) matrix[i] = [i]; for (let j = 0; j <= a.length; j++) matrix[0][j] = j; for (let i = 1; i <= b.length; i++) { for (let j = 1; j <= a.length; j++) { if (b.charAt(i - 1) === a.charAt(j - 1)) { matrix[i][j] = matrix[i - 1][j - 1]; } else { matrix[i][j] = Math.min( matrix[i - 1][j - 1] + 1, // substitution matrix[i][j - 1] + 1, // insertion matrix[i - 1][j] + 1 // deletion ); } } } return matrix[b.length][a.length]; } export function isFuzzyMatch(text: string, target: string, threshold = 0.8): boolean { const normalized = text.trim().toUpperCase(); const tar = target.toUpperCase(); if (normalized === tar) return true; if (normalized.includes(tar) || tar.includes(normalized)) return true; const distance = levenshteinDistance(normalized, tar); const maxLength = Math.max(normalized.length, tar.length); const similarity = 1 - distance / maxLength; return similarity >= threshold; } /** * Download a WhatsApp media file from the Graph API as a stream. * @param mediaId - The media ID from the WhatsApp webhook payload * @param accessToken - WHATSAPP_ACCESS_TOKEN * @returns { stream, mimeType, fileSize } */ export async function downloadMedia( mediaId: string, accessToken: string ): Promise<{ stream: Readable; mimeType: string; fileSize: number }> { // Step 1: Get the media URL const metaRes = await axios.get( `https://graph.facebook.com/${process.env.META_GRAPH_API_VERSION || 'v22.0'}/${mediaId}`, { headers: { Authorization: `Bearer ${accessToken}` } } ); const { url, mime_type, file_size } = metaRes.data; if (!url) throw new Error(`[downloadMedia] No URL returned for media ${mediaId}`); // Step 2: Download the binary content as a stream const mediaRes = await axios.get(url, { headers: { Authorization: `Bearer ${accessToken}` }, responseType: 'stream', timeout: 30_000 }); return { stream: mediaRes.data, mimeType: mime_type || 'application/octet-stream', fileSize: file_size || 0 }; }