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:
+39
-1
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user