Files
whatsapp/assets/js/chat-common.js
2026-02-20 11:21:06 -05:00

256 lines
13 KiB
JavaScript

// 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) : '<no body>';
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 `<div class="message-media"><a href="#" data-fullurl="${escapeHtml(fullUrl)}" onclick="openImageLightbox(this.dataset.fullurl); return false;" title="Abrir imagen"><img src="${escapeHtml(thumbUrl || fullUrl)}" alt="Imagen" class="message-media-image"></a></div>${content?`<div>${escapeHtml(content)}</div>`:''}`;
} else if (isValidMediaUrl(thumbUrl)) {
return `<div class="message-media"><img src="${escapeHtml(thumbUrl)}" alt="Imagen" class="message-media-image"></div>${content?`<div>${escapeHtml(content)}</div>`:''}`;
}
return `<div class="text-muted"><em>Imagen no disponible</em></div>`;
}
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 `<div class="message-media"><video controls><source src="${escapeHtml(src)}" type="video/mp4">Tu navegador no soporta video.</video></div>${content?`<div>${escapeHtml(content)}</div>`:''}`;
return `<div class="text-muted"><em>Video no disponible</em></div>`;
}
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 `<div class="text-muted"><em>Audio no disponible</em></div>`;
// 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 = `<div class="message-media"><audio controls><source src="${escapeHtml(src)}" type="audio/mpeg">Tu navegador no soporta audio.</audio></div>`;
if (audioDownload) {
html += `<div class="mt-2"><a href="${escapeHtml(audioDownload)}" class="btn btn-sm btn-outline-secondary" target="_blank" rel="noopener noreferrer">Descargar audio</a></div>`;
}
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 `<a href="${docUrl}" class="message-document" target="_blank" rel="noopener noreferrer"><i class="fas fa-file-pdf"></i><span>${escapeHtml(filename)}</span></a>`;
return `<div class="message-document" style="opacity:0.6;"><i class="fas fa-file-pdf"></i><span>${escapeHtml(filename)} (No disponible)</span></div>`;
}
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 = '<option value="">Seleccionar plantilla...</option>';
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 ({'&':'&amp;','<':'&lt;','>':'&gt;','"':'&quot;',"'":'&#039;'})[m]; });
}
window.escapeHtml = escapeHtml;