256 lines
13 KiB
JavaScript
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 ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[m]; });
|
|
}
|
|
window.escapeHtml = escapeHtml; |