// Shared helpers for chat UIs async function apiCall(endpoint, options = {}) { const defaultOptions = { method: 'GET', headers: { 'Content-Type': 'application/json' } }; const finalOptions = { ...defaultOptions, ...options }; if (options.body && typeof options.body === 'object') { finalOptions.method = 'POST'; finalOptions.body = JSON.stringify(options.body); } let url = endpoint; if (!/^https?:\/\//i.test(url) && !url.startsWith('api/')) url = 'api/' + url; const resp = await fetch(url, finalOptions); const text = await resp.text(); if (!resp.ok) { const snippet = text && text.length ? (text.length > 2000 ? text.substr(0,2000)+'... (truncated)' : text) : ''; console.error('apiCall HTTP error', resp.status, url, snippet); throw new Error('HTTP error! status: ' + resp.status + ' -- ' + snippet); } if (!text || text.trim() === '') return null; try { return JSON.parse(text); } catch (err) { console.error('apiCall parse error for', url, err); const snippet = text.length > 1000 ? text.substr(0,1000) : text; throw new Error('Invalid JSON response from ' + url + ': ' + err.message + ' -- response snippet: ' + snippet); } } function isValidMediaUrl(u) { return (typeof u === 'string') && u.trim() !== '' && u !== 'null' && u !== 'undefined'; } function ensureProxyUrl(u, download = false) { if (!isValidMediaUrl(u)) return ''; if (u.startsWith('/') || u.indexOf('/api/version/media-url.php') === 0) return u; // Si empieza con api/ agregar / al inicio if (u.startsWith('api/')) return '/' + u; try { const parsed = new URL(u); const host = parsed.host.toLowerCase(); if (host.includes('lookaside.fbsbx.com') || host.includes('facebook.com') || host.includes('graph.facebook.com')) { return `/api/version/media-url.php?url=${encodeURIComponent(u)}${download ? '&download=1' : ''}`; } } catch (e) {} return u; } function renderMediaMessage(msg) { if (!msg) return ''; const mediaType = msg.message_type || msg.media_type || 'text'; const content = msg.content || ''; const mediaExternal = msg.media_url_external || msg.media_url || ''; const localFile = msg.local_file || ''; const localThumb = msg.local_thumb || ''; const effective = isValidMediaUrl(mediaExternal) ? mediaExternal : (isValidMediaUrl(localFile) ? `/`+localFile : (isValidMediaUrl(localThumb) ? `/`+localThumb : (isValidMediaUrl(msg.media_url) ? msg.media_url : ''))); switch (mediaType) { case 'image': { // Compute thumbnail and full image URL following chat_window heuristics let thumbUrl = ''; if (isValidMediaUrl(localThumb)) { thumbUrl = `/${localThumb}`; } else if (isValidMediaUrl(localFile)) { thumbUrl = `/${localFile}`; } else if (isValidMediaUrl(effective) || isValidMediaUrl(mediaExternal)) { const external = isValidMediaUrl(effective) ? effective : mediaExternal; if (/lookaside\.fbsbx\.com|graph\.facebook\.com|facebook\.com/i.test(external)) { const m = external.match(/[?&]mid=([^&]+)/i); if (m && m[1]) { thumbUrl = `/api/version/media-url.php?id=${encodeURIComponent(m[1])}`; } else { thumbUrl = `/api/version/media-url.php?url=${encodeURIComponent(external)}`; } } else { // Route through media-url.php proxy (handles caching + auth) if (external.indexOf('api/get_media.php?id=') !== -1 || external.indexOf('api/version/media-url.php?id=') !== -1) { const parts = external.split('id='); const mid = parts[1] ? decodeURIComponent(parts[1]) : ''; thumbUrl = mid ? `/api/version/media-url.php?id=${encodeURIComponent(mid)}` : external; } else { thumbUrl = ensureProxyUrl(external) || external; } } } else if (isValidMediaUrl(msg.media_url)) { thumbUrl = msg.media_url; } // Full URL for opening (prefer local file then external with id or url) let fullUrl = ''; if (isValidMediaUrl(localFile)) { // Usar archivo local directamente sin proxy fullUrl = localFile.startsWith('/') ? localFile : `/${localFile}`; } else if (isValidMediaUrl(mediaExternal)) { let mid = ''; if (mediaExternal.indexOf('api/get_media.php?id=') !== -1 || mediaExternal.indexOf('api/version/media-url.php?id=') !== -1) { const parts = mediaExternal.split('id='); mid = parts[1] ? decodeURIComponent(parts[1]) : ''; } else { const m = mediaExternal.match(/[?&]mid=([^&]+)/i); if (m && m[1]) mid = decodeURIComponent(m[1]); } if (mid) fullUrl = `/api/version/media-url.php?id=${encodeURIComponent(mid)}`; else if (/^https?:\/\//i.test(mediaExternal)) fullUrl = `/api/version/media-url.php?url=${encodeURIComponent(mediaExternal)}`; else fullUrl = `/api/version/media-url.php?url=${encodeURIComponent(mediaExternal)}`; } else { fullUrl = ''; } if (isValidMediaUrl(fullUrl)) { return `
Imagen
${content?`
${escapeHtml(content)}
`:''}`; } else if (isValidMediaUrl(thumbUrl)) { return `
Imagen
${content?`
${escapeHtml(content)}
`:''}`; } return `
Imagen no disponible
`; } case 'video': { // Priorizar local_file sobre otras fuentes let src = ''; if (isValidMediaUrl(localFile)) { src = localFile.startsWith('/') ? localFile : `/${localFile}`; } else { src = ensureProxyUrl(effective || msg.media_url || ''); } if (src) return `
${content?`
${escapeHtml(content)}
`:''}`; return `
Video no disponible
`; } case 'audio': { // Priorizar local_file sobre otras fuentes let src = ''; if (isValidMediaUrl(localFile)) { src = localFile.startsWith('/') ? localFile : `/${localFile}`; } else { src = ensureProxyUrl(effective || msg.media_url || ''); } if (!src) return `
Audio no disponible
`; // build download link when possible (api/get_media.php?id=... or direct url or local file) let audioDownload = null; const candidate = isValidMediaUrl(localFile) ? localFile : (isValidMediaUrl(effective) ? effective : (isValidMediaUrl(msg.media_url) ? msg.media_url : null)); if (candidate && candidate.indexOf && candidate.indexOf('api/get_media.php?id=') !== -1) { const parts = candidate.split('id='); const mid = parts[1] ? parts[1] : ''; audioDownload = `/api/version/media-url.php?id=${encodeURIComponent(mid)}&download=1`; } else if (candidate && /^https?:\/\//i.test(candidate)) { audioDownload = `/api/version/media-url.php?url=${encodeURIComponent(candidate)}&download=1`; } else if (candidate && candidate === localFile) { // Usar archivo local directamente sin proxy audioDownload = localFile.startsWith('/') ? localFile : `/${localFile}`; } let html = `
`; if (audioDownload) { html += `
Descargar audio
`; } return html; } case 'document': { const filename = msg.filename || content || 'Documento'; let docUrl = ''; if (isValidMediaUrl(mediaExternal) && mediaExternal.indexOf('api/get_media.php?id=') !== -1) { const parts = mediaExternal.split('id='); docUrl = `/api/version/media-url.php?id=${encodeURIComponent(parts[1])}&download=1`; } else if (isValidMediaUrl(mediaExternal) && /^https?:\/\//i.test(mediaExternal)) { docUrl = `/api/version/media-url.php?url=${encodeURIComponent(mediaExternal)}&download=1`; } else if (isValidMediaUrl(localFile)) { // Usar archivo local directamente sin proxy docUrl = localFile.startsWith('/') ? localFile : `/${localFile}`; } if (docUrl) return `${escapeHtml(filename)}`; return `
${escapeHtml(filename)} (No disponible)
`; } default: return escapeHtml(content || ''); } } async function loadTemplatesInto(selector) { try { const resp = await apiCall('check_templates.php'); const sel = document.querySelector(selector); if (!sel) return; sel.innerHTML = ''; if (resp && resp.templates && Array.isArray(resp.templates)) { resp.templates.forEach(t => { const opt = document.createElement('option'); opt.value = t.name; const lang = t.language_code || t.language || 'en_US'; opt.textContent = `${t.display_name || t.name} (${lang})`; opt.dataset.language = lang; sel.appendChild(opt); }); } } catch (e) { console.error('loadTemplatesInto error', e); } } // FFmpeg helpers (client-side conversion) let _ffmpegInstance = null; let _ffmpegLoaded = false; async function ensureFFmpeg() { if (_ffmpegLoaded) return; if (typeof WebAssembly === 'undefined') throw new Error('WebAssembly no disponible'); if (!window.FFmpeg || !window.FFmpeg.createFFmpeg) { await new Promise((resolve, reject) => { const s = document.createElement('script'); s.src = 'https://unpkg.com/@ffmpeg/ffmpeg@0.11.8/dist/ffmpeg.min.js'; s.onload = resolve; s.onerror = reject; document.head.appendChild(s); }); } const { createFFmpeg, fetchFile } = window.FFmpeg; _ffmpegInstance = createFFmpeg({ log: false }); await _ffmpegInstance.load(); _ffmpegInstance._fetchFile = fetchFile; _ffmpegLoaded = true; } async function convertWebmToOgg(file) { await ensureFFmpeg(); const inName = 'input.webm'; const outName = 'output.ogg'; let data; if (_ffmpegInstance._fetchFile) data = await _ffmpegInstance._fetchFile(file); else data = new Uint8Array(await file.arrayBuffer()); _ffmpegInstance.FS('writeFile', inName, data); // Parámetros compatibles con WhatsApp Voice Messages: // -c:a libopus = códec OPUS (requerido) // -b:a 32k = bitrate bajo para mantener archivo < 512KB (ícono play) // -ar 48000 = sample rate 48kHz (estándar OPUS) // -ac 1 = mono (reduce tamaño) // -vbr on = variable bitrate para mejor calidad await _ffmpegInstance.run('-i', inName, '-c:a', 'libopus', '-b:a', '32k', '-ar', '48000', '-ac', '1', '-vbr', 'on', outName); const outData = _ffmpegInstance.FS('readFile', outName); const blob = new Blob([outData.buffer], { type: 'audio/ogg; codecs=opus' }); const newFile = new File([blob], (file.name || 'audio').replace(/\.[^/.]+$/, '') + '.ogg', { type: 'audio/ogg; codecs=opus' }); return newFile; } // Expose window.apiCall = apiCall; window.isValidMediaUrl = isValidMediaUrl; window.ensureProxyUrl = ensureProxyUrl; window.renderMediaMessage = renderMediaMessage; window.loadTemplatesInto = loadTemplatesInto; window.ensureFFmpeg = ensureFFmpeg; window.convertWebmToOgg = convertWebmToOgg; // small utility function escapeHtml(text) { if (!text) return ''; return (''+text).replace(/[&<>"']/g, function(m){ return ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[m]; }); } window.escapeHtml = escapeHtml;