From 9001aea8cd95a935c00b5b87d7e8bec5ebd92b06 Mon Sep 17 00:00:00 2001 From: Michelle Date: Tue, 24 Feb 2026 19:05:41 +0100 Subject: [PATCH] add avatar support for BBB --- redlight.db-shm | Bin 32768 -> 0 bytes redlight.db-wal | Bin 144232 -> 0 bytes server/config/bbb.js | 5 ++++- server/routes/auth.js | 30 ++++++++++++++++++++++++++++++ server/routes/rooms.js | 20 +++++++++++++++++--- 5 files changed, 51 insertions(+), 4 deletions(-) delete mode 100644 redlight.db-shm delete mode 100644 redlight.db-wal diff --git a/redlight.db-shm b/redlight.db-shm deleted file mode 100644 index 6ec64e46d82525970100d0e1098bebbc1f255d28..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32768 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 ZXcxeE1jZmL7>@ws5nRP1)U2dk;D2_#%4`4t diff --git a/server/config/bbb.js b/server/config/bbb.js index 5792fca..bdb46f1 100644 --- a/server/config/bbb.js +++ b/server/config/bbb.js @@ -66,7 +66,7 @@ export async function createMeeting(room, logoutURL) { return apiCall('create', params); } -export async function joinMeeting(uid, name, isModerator = false) { +export async function joinMeeting(uid, name, isModerator = false, avatarURL = null) { const { moderatorPW, attendeePW } = getRoomPasswords(uid); const params = { meetingID: uid, @@ -74,6 +74,9 @@ export async function joinMeeting(uid, name, isModerator = false) { password: isModerator ? moderatorPW : attendeePW, redirect: 'true', }; + if (avatarURL) { + params.avatarURL = avatarURL; + } return buildUrl('join', params); } diff --git a/server/routes/auth.js b/server/routes/auth.js index 25ee5ce..0d6a350 100644 --- a/server/routes/auth.js +++ b/server/routes/auth.js @@ -203,6 +203,36 @@ router.delete('/avatar', authenticateToken, async (req, res) => { } }); +// GET /api/auth/avatar/initials/:name - Generate SVG avatar from initials (public, BBB fetches this) +router.get('/avatar/initials/:name', (req, res) => { + const name = decodeURIComponent(req.params.name).trim(); + const color = req.query.color || generateColorFromName(name); + const initials = name + .split(' ') + .map(n => n[0]) + .join('') + .toUpperCase() + .slice(0, 2) || '?'; + + const svg = ` + + ${initials} + `; + + res.setHeader('Content-Type', 'image/svg+xml'); + res.setHeader('Cache-Control', 'public, max-age=3600'); + res.send(svg); +}); + +function generateColorFromName(name) { + let hash = 0; + for (let i = 0; i < name.length; i++) { + hash = name.charCodeAt(i) + ((hash << 5) - hash); + } + const hue = Math.abs(hash) % 360; + return `hsl(${hue}, 55%, 45%)`; +} + // GET /api/auth/avatar/:filename - Serve avatar image router.get('/avatar/:filename', (req, res) => { const filepath = path.join(uploadsDir, req.params.filename); diff --git a/server/routes/rooms.js b/server/routes/rooms.js index f2dcd35..63db1bd 100644 --- a/server/routes/rooms.js +++ b/server/routes/rooms.js @@ -12,6 +12,16 @@ import { const router = Router(); +// Build avatar URL for a user (uploaded image or generated initials) +function getUserAvatarURL(req, user) { + const baseUrl = `${req.protocol}://${req.get('host')}`; + if (user.avatar_image) { + return `${baseUrl}/api/auth/avatar/${user.avatar_image}`; + } + const color = user.avatar_color ? `?color=${encodeURIComponent(user.avatar_color)}` : ''; + return `${baseUrl}/api/auth/avatar/initials/${encodeURIComponent(user.name)}${color}`; +} + // GET /api/rooms - List user's rooms router.get('/', authenticateToken, async (req, res) => { try { @@ -199,7 +209,8 @@ router.post('/:uid/start', authenticateToken, async (req, res) => { } await createMeeting(room, `${req.protocol}://${req.get('host')}`); - const joinUrl = await joinMeeting(room.uid, req.user.name, true); + const avatarURL = getUserAvatarURL(req, req.user); + const joinUrl = await joinMeeting(room.uid, req.user.name, true, avatarURL); res.json({ joinUrl }); } catch (err) { console.error('Start meeting error:', err); @@ -229,7 +240,8 @@ router.post('/:uid/join', authenticateToken, async (req, res) => { } const isModerator = room.user_id === req.user.id || room.all_join_moderator; - const joinUrl = await joinMeeting(room.uid, req.user.name, isModerator); + const avatarURL = getUserAvatarURL(req, req.user); + const joinUrl = await joinMeeting(room.uid, req.user.name, isModerator, avatarURL); res.json({ joinUrl }); } catch (err) { console.error('Join meeting error:', err); @@ -335,7 +347,9 @@ router.post('/:uid/guest-join', async (req, res) => { isModerator = true; } - const joinUrl = await joinMeeting(room.uid, name.trim(), isModerator); + const baseUrl = `${req.protocol}://${req.get('host')}`; + const guestAvatarURL = `${baseUrl}/api/auth/avatar/initials/${encodeURIComponent(name.trim())}`; + const joinUrl = await joinMeeting(room.uid, name.trim(), isModerator, guestAvatarURL); res.json({ joinUrl }); } catch (err) { console.error('Guest join error:', err);