import crypto from 'crypto'; import xml2js from 'xml2js'; const BBB_URL = process.env.BBB_URL || 'https://your-bbb-server.com/bigbluebutton/api/'; const BBB_SECRET = process.env.BBB_SECRET || ''; function getChecksum(apiCall, params) { const queryString = new URLSearchParams(params).toString(); const raw = apiCall + queryString + BBB_SECRET; return crypto.createHash('sha256').update(raw).digest('hex'); } function buildUrl(apiCall, params = {}) { const checksum = getChecksum(apiCall, params); const queryString = new URLSearchParams({ ...params, checksum }).toString(); return `${BBB_URL}${apiCall}?${queryString}`; } async function apiCall(apiCallName, params = {}, xmlBody = null) { const url = buildUrl(apiCallName, params); // Logging: compact key=value style, filter sensitive params function formatUTC(d) { const pad = n => String(n).padStart(2, '0'); const Y = d.getUTCFullYear(); const M = pad(d.getUTCMonth() + 1); const D = pad(d.getUTCDate()); const h = pad(d.getUTCHours()); const m = pad(d.getUTCMinutes()); const s = pad(d.getUTCSeconds()); return `${Y}-${M}-${D} ${h}:${m}:${s} UTC`; } const SENSITIVE_KEYS = [/pass/i, /pwd/i, /password/i, /token/i, /jwt/i, /secret/i, /authorization/i, /auth/i, /api[_-]?key/i]; const isSensitive = key => SENSITIVE_KEYS.some(rx => rx.test(key)); function sanitizeParams(p) { try { const out = []; for (const k of Object.keys(p || {})) { if (k.toLowerCase() === 'checksum') continue; // never log checksum if (isSensitive(k)) { out.push(`${k}=[FILTERED]`); } else { let v = p[k]; if (typeof v === 'string' && v.length > 100) v = v.slice(0, 100) + '...[truncated]'; out.push(`${k}=${String(v)}`); } } return out.join('&') || '-'; } catch (e) { return '-'; } } const start = Date.now(); try { const fetchOptions = xmlBody ? { method: 'POST', headers: { 'Content-Type': 'application/xml' }, body: xmlBody } : {}; const response = await fetch(url, fetchOptions); const duration = Date.now() - start; const xml = await response.text(); const result = await xml2js.parseStringPromise(xml, { explicitArray: false, trim: true, }); // Compact log: time=... method=GET path=getMeetings format=xml status=200 duration=12.34 bbb_returncode=SUCCESS params=meetingID=123 try { const tokens = []; tokens.push(`time=${formatUTC(new Date())}`); tokens.push(`method=${xmlBody ? 'POST' : 'GET'}`); tokens.push(`path=${apiCallName}`); tokens.push(`format=xml`); tokens.push(`status=${response.status}`); tokens.push(`duration=${(duration).toFixed(2)}`); const bbbCode = result && result.response && result.response.returncode ? result.response.returncode : '-'; tokens.push(`bbb_returncode=${bbbCode}`); const safeParams = sanitizeParams(params); tokens.push(`params=${safeParams}`); console.info(tokens.join(' ')); } catch (e) { // ignore logging errors } return result.response; } catch (error) { const duration = Date.now() - start; console.error(`BBB API error (${apiCallName}) status=error duration=${(duration).toFixed(2)} err=${error.message}`); throw error; } } // Generate deterministic passwords from room UID function getRoomPasswords(uid) { const modPw = crypto.createHash('sha256').update(uid + '_mod_' + BBB_SECRET).digest('hex').substring(0, 16); const attPw = crypto.createHash('sha256').update(uid + '_att_' + BBB_SECRET).digest('hex').substring(0, 16); return { moderatorPW: modPw, attendeePW: attPw }; } export async function createMeeting(room, logoutURL, loginURL = null, presentationUrl = null) { const { moderatorPW, attendeePW } = getRoomPasswords(room.uid); // Build welcome message with guest invite link let welcome = room.welcome_message || t('defaultWelcome'); if (logoutURL) { const guestLink = `${logoutURL}/join/${room.uid}`; welcome += `

To invite other participants, share this link:
${guestLink}`; if (room.access_code) { welcome += `
Access Code: ${room.access_code}`; } } const params = { meetingID: room.uid, name: room.name, attendeePW, moderatorPW, welcome, record: room.record_meeting ? 'true' : 'false', autoStartRecording: 'false', allowStartStopRecording: 'true', muteOnStart: room.mute_on_join ? 'true' : 'false', 'meta_bbb-origin': 'Redlight', 'meta_bbb-origin-server-name': 'Redlight', }; if (logoutURL) { params.logoutURL = logoutURL; } if (loginURL) { params.loginURL = loginURL; } if (room.max_participants > 0) { params.maxParticipants = room.max_participants.toString(); } if (room.access_code) { params.lockSettingsLockOnJoin = 'true'; } // Build optional presentation XML body – escape URL to prevent XML injection let xmlBody = null; if (presentationUrl) { const safeUrl = presentationUrl .replace(/&/g, '&') .replace(/"/g, '"') .replace(//g, '>'); xmlBody = ``; } return apiCall('create', params, xmlBody); } export async function joinMeeting(uid, name, isModerator = false, avatarURL = null) { const { moderatorPW, attendeePW } = getRoomPasswords(uid); const params = { meetingID: uid, fullName: name, password: isModerator ? moderatorPW : attendeePW, redirect: 'true', }; if (avatarURL) { params.avatarURL = avatarURL; } return buildUrl('join', params); } export async function endMeeting(uid) { const { moderatorPW } = getRoomPasswords(uid); return apiCall('end', { meetingID: uid, password: moderatorPW }); } export async function getMeetingInfo(uid) { return apiCall('getMeetingInfo', { meetingID: uid }); } export async function isMeetingRunning(uid) { const result = await apiCall('isMeetingRunning', { meetingID: uid }); return result.running === 'true'; } export async function getMeetings() { return apiCall('getMeetings', {}); } export async function getRecordings(meetingID) { const params = meetingID ? { meetingID } : {}; const result = await apiCall('getRecordings', params); if (result.returncode !== 'SUCCESS' || !result.recordings) { return []; } const recordings = result.recordings.recording; if (!recordings) return []; return Array.isArray(recordings) ? recordings : [recordings]; } export async function getRecordingByRecordId(recordID) { const result = await apiCall('getRecordings', { recordID }); if (result.returncode !== 'SUCCESS' || !result.recordings) { return null; } const recordings = result.recordings.recording; if (!recordings) return null; const arr = Array.isArray(recordings) ? recordings : [recordings]; return arr[0] || null; } export async function deleteRecording(recordID) { return apiCall('deleteRecordings', { recordID }); } export async function publishRecording(recordID, publish) { return apiCall('publishRecordings', { recordID, publish: publish ? 'true' : 'false' }); } export { getRoomPasswords };