diff --git a/assets/js/chat-common.js b/assets/js/chat-common.js new file mode 100644 index 0000000..2705a9d --- /dev/null +++ b/assets/js/chat-common.js @@ -0,0 +1,168 @@ +// 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; + 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': { + const thumb = effective || ''; + const full = isValidMediaUrl(localFile) ? `/api/version/media-url.php?local=${encodeURIComponent(localFile)}` : (isValidMediaUrl(mediaExternal) ? `/api/version/media-url.php?url=${encodeURIComponent(mediaExternal)}` : (msg.media_url || '')); + if (thumb) { + return `
Imagen
${content?`
${escapeHtml(content)}
`:''}`; + } + return `
Imagen no disponible
`; + } + case 'video': { + const src = ensureProxyUrl(effective || msg.media_url || ''); + if (src) return `
${content?`
${escapeHtml(content)}
`:''}`; + return `
Video no disponible
`; + } + case 'audio': { + const src = ensureProxyUrl(effective || msg.media_url || ''); + if (src) return `
`; + return `
Audio no disponible
`; + } + 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)) { + docUrl = `/api/version/media-url.php?local=${encodeURIComponent(localFile)}&download=1`; + } + 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); + await _ffmpegInstance.run('-i', inName, '-c:a', 'libopus', '-b:a', '64k', outName); + const outData = _ffmpegInstance.FS('readFile', outName); + const blob = new Blob([outData.buffer], { type: 'audio/ogg' }); + const newFile = new File([blob], (file.name || 'audio').replace(/\.[^/.]+$/, '') + '.ogg', { type: 'audio/ogg' }); + 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; \ No newline at end of file diff --git a/conversations.php b/conversations.php index 750ec9d..ea5a412 100644 --- a/conversations.php +++ b/conversations.php @@ -579,9 +579,28 @@ +
+ +
+ +
+