feat(app-store): add BigBlueButton video conferencing integration#29453
feat(app-store): add BigBlueButton video conferencing integration#29453ThaiTrevor wants to merge 2 commits into
Conversation
Adds a new conferencing app for BigBlueButton (closes calcom#1985 / CAL-3105). The integration follows the jitsivideo pattern with a server-side VideoApiAdapter that authenticates BBB API requests using the standard SHA-1 shared-secret checksum, so meetings are actually created on the BBB server (rather than just generating a static link). Admin configures the BBB server URL and shared secret via the standard app-keys flow (packages/app-store/bigbluebutton/zod.ts). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Welcome to Cal.diy, @ThaiTrevor! Thanks for opening this pull request. A few things to keep in mind:
A maintainer will review your PR soon. Thanks for contributing! |
|
|
|
Hi maintainers! Could one of you please add the |
📝 WalkthroughWalkthroughThis pull request adds a complete BigBlueButton video conferencing integration to the Cal.com app store. The implementation includes cryptographic utilities for signed API requests, Zod schemas for configuration validation, a VideoApiAdapter for managing meeting lifecycle (creation, deletion, metadata resolution), a Next.js API handler for credential installation with team authorization, and full registry wiring into the app-store system. The package is self-contained with internal exports and external dependencies on 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Warning There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure. 🔧 ESLint
ESLint skipped: no ESLint configuration detected in root package.json. To enable, add Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 5
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@packages/app-store/bigbluebutton/api/add.ts`:
- Around line 18-26: Validate the teamId from req.query before coercing to
Number: check that teamId is a single string and matches a numeric pattern (or
use Number.isInteger after parsing) and if invalid respond with
res.status(400).send(...) before calling throwIfNotHaveAdminAccessToTeam or
building installForObject; when valid, convert once to a Number (e.g., const
parsedTeamId = Number(teamId)) and pass parsedTeamId to
throwIfNotHaveAdminAccessToTeam and to installForObject (or use parsedTeamId ? {
teamId: parsedTeamId } : { userId: req.session.user.id }) to avoid NaN or array
inputs.
- Around line 28-44: The current findFirst → create flow in add.ts
(prisma.credential.findFirst / prisma.credential.create) is race-prone; make
installation idempotent by enforcing uniqueness at the DB boundary: add a
composite unique constraint to the Credential model in
packages/prisma/schema.prisma (e.g. @@unique([appId, type, userId, teamId]) or
an appropriate subset that represents a single install identity), run a
migration, and then update add.ts to use a conflict-safe operation (replace the
findFirst/create pair with a single upsert or keep create wrapped with handling
for unique-violation errors) so concurrent requests cannot create duplicate
credentials.
In `@packages/app-store/bigbluebutton/lib/VideoApiAdapter.ts`:
- Line 79: The log at VideoApiAdapter (in the method handling the BigBlueButton
create response) currently prints the entire remote response body; change it to
avoid logging raw body content by parsing the response safely and logging only
normalized error fields such as response.status, response.statusCode (if
present), and any explicit error/code/message fields (e.g., body.code or
body.message), or a short sanitized summary (first N chars) if structured fields
are absent; update the log.error call to include only those extracted fields and
context (e.g., "BBB create failed" + status/code/message) instead of the raw
body string.
- Around line 75-80: Replace the two plain throws in VideoApiAdapter.ts with
ErrorWithCode instances: when the fetch/create fails (the "Unable to create
BigBlueButton meeting" throw) throw new ErrorWithCode with a suitable code
(e.g., "EXTERNAL_SERVICE_ERROR" or "UNAVAILABLE") and the same message; when the
response body indicates failure (the "BigBlueButton rejected the create request"
case) throw ErrorWithCode including a descriptive code (e.g., "BAD_RESPONSE" or
"EXTERNAL_SERVICE_ERROR") and include the response body in the error message or
metadata for diagnostics; import ErrorWithCode from your project's error
utilities and keep the existing log.error call intact.
- Around line 103-107: The deleteMeeting implementation is calling
buildApiUrl("end", ...) with password: "" which will fail to end meetings;
update deleteMeeting to pass the moderator password into buildApiUrl (either by
adding a moderatorPassword parameter to deleteMeeting or by retrieving the
stored moderator/moderatorPW for meetingID from wherever meetings are persisted)
and use that value instead of an empty string when constructing endUrl; ensure
references to buildApiUrl, deleteMeeting, meetingID and password are updated so
the real moderator password is supplied to the BBB end call.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: 91aa76c9-5cf2-4c15-850f-2aeb3cf337ee
⛔ Files ignored due to path filters (1)
packages/app-store/bigbluebutton/static/icon.svgis excluded by!**/*.svg
📒 Files selected for processing (20)
.bounty_pr.jsonpackages/app-store/apps.keys-schemas.generated.tspackages/app-store/apps.metadata.generated.tspackages/app-store/apps.schemas.generated.tspackages/app-store/apps.server.generated.tspackages/app-store/bigbluebutton/DESCRIPTION.mdpackages/app-store/bigbluebutton/_metadata.tspackages/app-store/bigbluebutton/api/add.tspackages/app-store/bigbluebutton/api/index.tspackages/app-store/bigbluebutton/index.tspackages/app-store/bigbluebutton/lib/VideoApiAdapter.tspackages/app-store/bigbluebutton/lib/bbb.test.tspackages/app-store/bigbluebutton/lib/bbb.tspackages/app-store/bigbluebutton/lib/getBigBlueButtonAppKeys.tspackages/app-store/bigbluebutton/lib/index.tspackages/app-store/bigbluebutton/package.jsonpackages/app-store/bigbluebutton/zod.tspackages/app-store/bookerApps.metadata.generated.tspackages/app-store/video.adapters.generated.tspackages/i18n/locales/en/common.json
| const { teamId, returnTo } = req.query; | ||
|
|
||
| await throwIfNotHaveAdminAccessToTeam({ | ||
| teamId: teamId ? Number(teamId) : null, | ||
| userId: req.session.user.id, | ||
| }); | ||
|
|
||
| const installForObject = teamId ? { teamId: Number(teamId) } : { userId: req.session.user.id }; | ||
| const appType = "bigbluebutton_video"; |
There was a problem hiding this comment.
Validate teamId before numeric coercion.
Number(teamId) can become NaN (e.g., array/invalid input). Reject invalid teamId with 400 before auth/query logic to avoid ambiguous behavior.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/app-store/bigbluebutton/api/add.ts` around lines 18 - 26, Validate
the teamId from req.query before coercing to Number: check that teamId is a
single string and matches a numeric pattern (or use Number.isInteger after
parsing) and if invalid respond with res.status(400).send(...) before calling
throwIfNotHaveAdminAccessToTeam or building installForObject; when valid,
convert once to a Number (e.g., const parsedTeamId = Number(teamId)) and pass
parsedTeamId to throwIfNotHaveAdminAccessToTeam and to installForObject (or use
parsedTeamId ? { teamId: parsedTeamId } : { userId: req.session.user.id }) to
avoid NaN or array inputs.
| const alreadyInstalled = await prisma.credential.findFirst({ | ||
| where: { | ||
| type: appType, | ||
| ...installForObject, | ||
| }, | ||
| }); | ||
| if (alreadyInstalled) { | ||
| throw new Error("Already installed"); | ||
| } | ||
| const installation = await prisma.credential.create({ | ||
| data: { | ||
| type: appType, | ||
| key: {}, | ||
| ...installForObject, | ||
| appId: "bigbluebutton", | ||
| }, | ||
| }); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify whether Credential has a uniqueness constraint covering install identity.
# Expected: a unique index that prevents duplicate bigbluebutton installs per owner scope.
fd -i "schema.prisma" | head -n 5
rg -n -C4 'model\s+Credential|@@unique|\bunique\b|appId|type|teamId|userId' $(fd -i "schema.prisma")Repository: calcom/cal.diy
Length of output: 50371
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Find and print the full `model Credential` blocks from schema.prisma files in-repo.
for f in $(fd -i "schema.prisma"); do
if rg -n "^[[:space:]]*model Credential[[:space:]]*{" "$f" >/dev/null; then
echo "===== $f ====="
start=$(rg -n "^[[:space:]]*model Credential[[:space:]]*{" "$f" | head -n1 | cut -d: -f1)
awk -v s="$start" '
NR>=s {
print
if (NR>s && $0 ~ /^[[:space:]]*}[[:space:]]*$/) { exit }
}
' "$f"
echo
fi
doneRepository: calcom/cal.diy
Length of output: 354
🏁 Script executed:
#!/bin/bash
set -euo pipefail
for f in $(fd -i "schema.prisma"); do
# Locate the start line of `model Credential {` without regex braces issues
start=$(rg -n -F "model Credential {" "$f" | head -n1 | cut -d: -f1 || true)
if [ -n "${start:-}" ]; then
echo "===== $f (model Credential starts at line $start) ====="
awk -v s="$start" '
NR>=s {
print
# Heuristic: stop at the first standalone closing brace after the block starts
if (NR>s && $0 ~ /^[[:space:]]*}[[:space:]]*$/) exit
}
' "$f"
echo
fi
doneRepository: calcom/cal.diy
Length of output: 1431
Make installation idempotent at the DB boundary.
packages/prisma/schema.prisma’s model Credential has only @@index(...) constraints (no @@unique(...)) for the install identity fields (appId, type, userId/teamId). That makes the findFirst → create flow in packages/app-store/bigbluebutton/api/add.ts race-prone, so concurrent requests can create duplicate credentials. Add a DB-enforced unique constraint (or conflict-safe upsert/unique-violation handling) to make installation idempotent.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/app-store/bigbluebutton/api/add.ts` around lines 28 - 44, The
current findFirst → create flow in add.ts (prisma.credential.findFirst /
prisma.credential.create) is race-prone; make installation idempotent by
enforcing uniqueness at the DB boundary: add a composite unique constraint to
the Credential model in packages/prisma/schema.prisma (e.g. @@unique([appId,
type, userId, teamId]) or an appropriate subset that represents a single install
identity), run a migration, and then update add.ts to use a conflict-safe
operation (replace the findFirst/create pair with a single upsert or keep create
wrapped with handling for unique-violation errors) so concurrent requests cannot
create duplicate credentials.
| throw new Error("Unable to create BigBlueButton meeting"); | ||
| } | ||
| const body = await response.text(); | ||
| if (!/<returncode>SUCCESS<\/returncode>/i.test(body)) { | ||
| log.error(`BigBlueButton create call returned a failure: ${body}`); | ||
| throw new Error("BigBlueButton rejected the create request"); |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Use ErrorWithCode instead of generic Error in this adapter.
Lines 75 and 80 throw plain Error in a non-tRPC file; this violates the project error-handling convention and reduces structured handling upstream.
As per coding guidelines: "Use ErrorWithCode for errors in non-tRPC files (services, repositories, utilities); use TRPCError only in tRPC routers".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/app-store/bigbluebutton/lib/VideoApiAdapter.ts` around lines 75 -
80, Replace the two plain throws in VideoApiAdapter.ts with ErrorWithCode
instances: when the fetch/create fails (the "Unable to create BigBlueButton
meeting" throw) throw new ErrorWithCode with a suitable code (e.g.,
"EXTERNAL_SERVICE_ERROR" or "UNAVAILABLE") and the same message; when the
response body indicates failure (the "BigBlueButton rejected the create request"
case) throw ErrorWithCode including a descriptive code (e.g., "BAD_RESPONSE" or
"EXTERNAL_SERVICE_ERROR") and include the response body in the error message or
metadata for diagnostics; import ErrorWithCode from your project's error
utilities and keep the existing log.error call intact.
| } | ||
| const body = await response.text(); | ||
| if (!/<returncode>SUCCESS<\/returncode>/i.test(body)) { | ||
| log.error(`BigBlueButton create call returned a failure: ${body}`); |
There was a problem hiding this comment.
Avoid logging full BBB response body on failures.
Line 79 logs raw remote body, which can include meeting metadata/user-provided text. Prefer logging normalized error fields/status only.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/app-store/bigbluebutton/lib/VideoApiAdapter.ts` at line 79, The log
at VideoApiAdapter (in the method handling the BigBlueButton create response)
currently prints the entire remote response body; change it to avoid logging raw
body content by parsing the response safely and logging only normalized error
fields such as response.status, response.statusCode (if present), and any
explicit error/code/message fields (e.g., body.code or body.message), or a short
sanitized summary (first N chars) if structured fields are absent; update the
log.error call to include only those extracted fields and context (e.g., "BBB
create failed" + status/code/message) instead of the raw body string.
| const endUrl = buildApiUrl( | ||
| { url: bbb_url, secret: bbb_secret }, | ||
| "end", | ||
| { meetingID, password: "" } | ||
| ); |
There was a problem hiding this comment.
deleteMeeting is calling BBB end without a moderator password.
At Line 106, password: "" makes the end request very likely invalid for BBB, so meetings may never actually terminate.
Suggested fix
- const endUrl = buildApiUrl(
- { url: bbb_url, secret: bbb_secret },
- "end",
- { meetingID, password: "" }
- );
+ const endUrl = buildApiUrl(
+ { url: bbb_url, secret: bbb_secret },
+ "end",
+ { meetingID, password: /* moderator password for this meeting */ }
+ );This likely needs plumbing the moderator password into deleteMeeting (or persisting it where teardown can retrieve it).
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@packages/app-store/bigbluebutton/lib/VideoApiAdapter.ts` around lines 103 -
107, The deleteMeeting implementation is calling buildApiUrl("end", ...) with
password: "" which will fail to end meetings; update deleteMeeting to pass the
moderator password into buildApiUrl (either by adding a moderatorPassword
parameter to deleteMeeting or by retrieving the stored moderator/moderatorPW for
meetingID from wherever meetings are persisted) and use that value instead of an
empty string when constructing endUrl; ensure references to buildApiUrl,
deleteMeeting, meetingID and password are updated so the real moderator password
is supplied to the BBB end call.
|
@CLAassistant recheck |
Summary
Adds a BigBlueButton conferencing app to the app-store, addressing the
long-standing request in #1985 / CAL-3105.
The integration follows the existing
jitsivideopattern (dynamic location,shared-secret auth — no OAuth) and exposes a real
VideoApiAdapterthatactually provisions meetings on the BBB server, rather than only producing
a static join URL:
packages/app-store/bigbluebutton/with_metadata.ts,package.json,index.ts,zod.ts,api/add.ts,lib/VideoApiAdapter.ts,lib/bbb.ts,lib/getBigBlueButtonAppKeys.ts,static/icon.svg, andDESCRIPTION.md.bbb_url,bbb_secret) following the samepattern as Daily / Jitsi.
lib/bbb.tsimplements the standard BBB SHA-1 shared-secret checksum(
<callName><queryString><sharedSecret>) per the BBB API security model.VideoApiAdapter.createMeeting()callscreatethen returns a signedjoinURL (redirect=true).deleteMeeting()callsend.apps.metadata.generated.ts,apps.server.generated.ts,video.adapters.generated.ts,apps.keys-schemas.generated.ts,apps.schemas.generated.ts,bookerApps.metadata.generated.ts.cal_provide_bigbluebutton_meeting_urllocale string.Closes #1985
Test plan
packages/app-store/bigbluebutton/lib/bbb.test.tscovering
buildQueryString,computeChecksum, andbuildApiUrl(including the trailing-slash and no-params edge cases).
computeChecksum('create', 'name=Test&meetingID=abc', 'supersecret')matches
sha1('createname=Test&meetingID=abcsupersecret').yarn test packages/app-store/bigbluebuttonand
yarn type-check./apps, set BBB server URL andshared secret in
/settings/admin/apps/bigbluebutton, and create abooking using the BigBlueButton location.
Notes
jitsivideoadapter as the reference template; this is theclosest existing analogue (shared-secret auth, dynamic link, no OAuth).
bigbluebuttonandgetVideoAdapters.tsalready strips_videofrom credential types, sothe existing lookup fallback resolves
bigbluebutton_videocorrectly.the standard
app.keysadmin-settings flow perCONTRIBUTING.md.icon.svgis included. The maintainer may want to swap itfor the official BigBlueButton wordmark before merging.