feat(chat): GIF picker via server-proxied GIPHY (#5) (batch87)

- Server: GET /api/gifs proxies GIPHY search/trending. The API key is read from the server
  env only (config.GIPHY_API_KEY, from the gitignored .env) and NEVER reaches the browser;
  the picker is hidden when it isn't configured.
- Emoji picker gains a GIF tab (separated from the emoji categories) with a search box + a 2-col
  grid, "Powered by GIPHY" attribution. Clicking a GIF sends it immediately.
- GIFs are HOTLINKED to GIPHY's CDN (their terms require this — no re-hosting): the message body
  is the GIF url, and a body that is a lone GIF url renders inline as the animated GIF (reusing
  the image/lightbox path). Sidebar previews + notifications show "🎞️ GIF", not the raw url.

Key is NOT in git — set as GIPHY_API_KEY in the server .env.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 15:30:44 +05:30
parent c3dc47a94c
commit 02075fbd47
3 changed files with 109 additions and 9 deletions
+39 -1
View File
@@ -113,7 +113,20 @@ const API_KEY_SCOPES = ['report:read', 'audit:read'];
const { onlineAgents, meetingRooms, groupCalls, dmCalls } = require('./presence');
const CALLS = require('./calls');
require('./reminders'); // start the 10-minute meeting-reminder loop
const { REC_DIR, TRANS_DIR, UPLOADS_DIR, SESSION_TTL, REFRESH_TTL, LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET, LIVEKIT_ENABLED, PUBLIC_BASE_URL } = require('./config');
const { REC_DIR, TRANS_DIR, UPLOADS_DIR, SESSION_TTL, REFRESH_TTL, LIVEKIT_URL, LIVEKIT_API_KEY, LIVEKIT_API_SECRET, LIVEKIT_ENABLED, PUBLIC_BASE_URL, GIPHY_API_KEY } = require('./config');
const https = require('https');
// Small GET-JSON helper for the GIPHY proxy (keeps the key server-side).
function fetchJSON(url) {
return new Promise((resolve, reject) => {
const req = https.get(url, (res) => {
if (res.statusCode !== 200) { res.resume(); return reject(new Error('upstream ' + res.statusCode)); }
let buf = ''; res.on('data', (c) => { buf += c; if (buf.length > 4 * 1024 * 1024) { req.destroy(); reject(new Error('too large')); } });
res.on('end', () => { try { resolve(JSON.parse(buf)); } catch (e) { reject(e); } });
});
req.on('error', reject);
req.setTimeout(8000, () => { req.destroy(); reject(new Error('timeout')); });
});
}
const mailer = require('./mailer');
// Basic email validation for external meeting invitees (#4).
const isEmail = (s) => typeof s === 'string' && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(s.trim());
@@ -955,6 +968,31 @@ route('GET', '/api/meetings/config', (req, res) => {
json(res, 200, { sfu: LIVEKIT_ENABLED, url: LIVEKIT_ENABLED ? LIVEKIT_URL : '' });
});
// #5 GIF search — server-side GIPHY proxy so the API key never reaches the browser. Empty q → trending.
route('GET', '/api/gifs', async (req, res) => {
const u = currentUser(req);
if (!u) return json(res, 401, { error: 'unauthorized' });
if (!GIPHY_API_KEY) return json(res, 200, { enabled: false, gifs: [] });
const p = new URLSearchParams(req.url.split('?')[1] || '');
const q = (p.get('q') || '').trim();
const offset = Math.max(0, Math.min(200, Number(p.get('offset')) || 0));
const limit = 24;
const base = 'https://api.giphy.com/v1/gifs/' + (q ? 'search' : 'trending');
const url = base + '?api_key=' + encodeURIComponent(GIPHY_API_KEY)
+ (q ? ('&q=' + encodeURIComponent(q)) : '')
+ '&limit=' + limit + '&offset=' + offset + '&rating=pg-13&bundle=messaging_non_clips';
try {
const data = await fetchJSON(url);
const gifs = (data && Array.isArray(data.data) ? data.data : []).map((g) => {
const im = g.images || {};
const full = (im.downsized_medium || im.fixed_height || im.original || {});
const prev = (im.fixed_width_small || im.fixed_height_small || im.preview_gif || full || {});
return { id: g.id, url: full.url || '', preview: prev.url || full.url || '', w: +full.width || 0, h: +full.height || 0, title: g.title || 'GIF' };
}).filter((g) => g.url);
json(res, 200, { enabled: true, gifs, offset: offset + limit });
} catch (e) { json(res, 502, { error: 'gif search failed' }); }
});
// The web build currently on the server (home.html's __BUILD marker). Long-running clients poll this and
// offer a Refresh when it changes. This matters because the desktop app now CLOSES TO TRAY — it can run
// for weeks without ever reloading the page, so it would silently keep serving stale code after a deploy.