diff --git a/apps/web/components/apps/AppSetupPage.tsx b/apps/web/components/apps/AppSetupPage.tsx index 88acf8c89bbb76..0110bdce792537 100644 --- a/apps/web/components/apps/AppSetupPage.tsx +++ b/apps/web/components/apps/AppSetupPage.tsx @@ -2,6 +2,7 @@ import { DynamicComponent } from "@calcom/app-store/_components/DynamicComponent import dynamic from "next/dynamic"; export const AppSetupMap = { + bigbluebutton: dynamic(() => import("@calcom/web/components/apps/bigbluebutton/Setup")), alby: dynamic(() => import("@calcom/web/components/apps/alby/Setup")), "apple-calendar": dynamic(() => import("@calcom/web/components/apps/applecalendar/Setup")), exchange: dynamic(() => import("@calcom/web/components/apps/exchangecalendar/Setup")), diff --git a/apps/web/components/apps/bigbluebutton/Setup.tsx b/apps/web/components/apps/bigbluebutton/Setup.tsx new file mode 100644 index 00000000000000..b182702603959f --- /dev/null +++ b/apps/web/components/apps/bigbluebutton/Setup.tsx @@ -0,0 +1,95 @@ +import { useRouter } from "next/navigation"; +import { useState } from "react"; +import { useForm } from "react-hook-form"; +import { Toaster } from "sonner"; + +import { useLocale } from "@calcom/lib/hooks/useLocale"; +import { Alert } from "@calcom/ui/components/alert"; +import { Button } from "@calcom/ui/components/button"; +import { Form } from "@calcom/ui/components/form"; +import { TextField } from "@calcom/ui/components/form"; +import { PasswordField } from "@calcom/ui/components/form"; + +export default function BigBlueButtonSetup() { + const { t } = useLocale(); + const router = useRouter(); + const form = useForm({ + defaultValues: { + serverUrl: "", + sharedSecret: "", + }, + }); + + const [errorMessage, setErrorMessage] = useState(""); + + return ( +
+
+
+
+ {/* eslint-disable @next/next/no-img-element */} + BigBlueButton +
+
+

Connect BigBlueButton

+
+ Enter your BigBlueButton server URL and shared secret to enable video conferencing. +
+
+
{ + setErrorMessage(""); + const res = await fetch("/api/integrations/bigbluebutton/add", { + method: "POST", + body: JSON.stringify(values), + headers: { + "Content-Type": "application/json", + }, + }); + const json = await res.json(); + if (!res.ok) { + setErrorMessage(json?.message || t("something_went_wrong")); + } else { + router.push(json.url); + } + }}> +
+ + +
+ + {errorMessage && } +
+ + +
+ +
+
+
+ +
+
+ ); +} diff --git a/packages/app-store/apps.keys-schemas.generated.ts b/packages/app-store/apps.keys-schemas.generated.ts index 5df66d19f7411c..658ddebe623381 100644 --- a/packages/app-store/apps.keys-schemas.generated.ts +++ b/packages/app-store/apps.keys-schemas.generated.ts @@ -22,6 +22,7 @@ import { appKeysSchema as insihts_zod_ts } from "./insihts/zod"; import { appKeysSchema as intercom_zod_ts } from "./intercom/zod"; import { appKeysSchema as jelly_zod_ts } from "./jelly/zod"; import { appKeysSchema as jitsivideo_zod_ts } from "./jitsivideo/zod"; +import { appKeysSchema as bigbluebutton_zod_ts } from "./bigbluebutton/zod"; import { appKeysSchema as larkcalendar_zod_ts } from "./larkcalendar/zod"; import { appKeysSchema as lyra_zod_ts } from "./lyra/zod"; import { appKeysSchema as make_zod_ts } from "./make/zod"; @@ -74,6 +75,7 @@ export const appKeysSchemas = { intercom: intercom_zod_ts, jelly: jelly_zod_ts, jitsivideo: jitsivideo_zod_ts, + bigbluebutton: bigbluebutton_zod_ts, larkcalendar: larkcalendar_zod_ts, lyra: lyra_zod_ts, make: make_zod_ts, diff --git a/packages/app-store/apps.metadata.generated.ts b/packages/app-store/apps.metadata.generated.ts index 4857b87fdac178..7e085e66dd4c16 100644 --- a/packages/app-store/apps.metadata.generated.ts +++ b/packages/app-store/apps.metadata.generated.ts @@ -52,6 +52,7 @@ import insihts_config_json from "./insihts/config.json"; import intercom_config_json from "./intercom/config.json"; import jelly_config_json from "./jelly/config.json"; import { metadata as jitsivideo__metadata_ts } from "./jitsivideo/_metadata"; +import { metadata as bigbluebutton__metadata_ts } from "./bigbluebutton/_metadata"; import { metadata as larkcalendar__metadata_ts } from "./larkcalendar/_metadata"; import lindy_config_json from "./lindy/config.json"; import linear_config_json from "./linear/config.json"; @@ -164,6 +165,7 @@ export const appStoreMetadata = { intercom: intercom_config_json, jelly: jelly_config_json, jitsivideo: jitsivideo__metadata_ts, + bigbluebutton: bigbluebutton__metadata_ts, larkcalendar: larkcalendar__metadata_ts, lindy: lindy_config_json, linear: linear_config_json, diff --git a/packages/app-store/apps.server.generated.ts b/packages/app-store/apps.server.generated.ts index 495e07786d6754..8584116642acba 100644 --- a/packages/app-store/apps.server.generated.ts +++ b/packages/app-store/apps.server.generated.ts @@ -38,6 +38,7 @@ export const apiHandlers = { intercom: import("./intercom/api"), jelly: import("./jelly/api"), jitsivideo: import("./jitsivideo/api"), + bigbluebutton: import("./bigbluebutton/api"), larkcalendar: import("./larkcalendar/api"), linear: import("./linear/api"), lyra: import("./lyra/api"), diff --git a/packages/app-store/bigbluebutton/DESCRIPTION.md b/packages/app-store/bigbluebutton/DESCRIPTION.md new file mode 100644 index 00000000000000..166f78e7f1c076 --- /dev/null +++ b/packages/app-store/bigbluebutton/DESCRIPTION.md @@ -0,0 +1,44 @@ +--- +items: + - icon.svg +--- + +BigBlueButton is an open-source video conferencing system designed for education and business. Host unlimited meetings on your own server with features like screen sharing, whiteboard, breakout rooms, and recording capabilities. + +## Features + +- 🎥 HD video conferencing +- 📱 Works on desktop and mobile +- 🎯 Built for education and business +- 🔒 Self-hosted for privacy and control +- 📊 Screen sharing and presentation tools +- ✏️ Interactive whiteboard +- 👥 Breakout rooms for small groups +- 📹 Optional meeting recording + +## Setup + +1. **BigBlueButton Server**: You need access to a BigBlueButton server. You can: + - Set up your own server following the [BigBlueButton installation guide](https://docs.bigbluebutton.org/install/install-overview) + - Use a hosted BigBlueButton provider + - Use BigBlueButton's demo server for testing (not recommended for production) + +2. **Server URL**: Enter your BigBlueButton server URL (e.g., `https://your-bbb-server.com`) + +3. **Shared Secret**: Enter your BigBlueButton shared secret (found in `/opt/bbb/conf/salt` on your server) + +## How it works + +When someone books a meeting with you: + +1. A unique BigBlueButton meeting room is automatically created +2. The meeting host (you) receives a moderator link +3. Meeting attendees receive participant links +4. The meeting room is automatically deleted when the booking is cancelled + +## Security + +- All communications are encrypted end-to-end +- Meeting rooms are created on-demand and deleted after use +- Attendee access is controlled by unique passwords +- Self-hosted deployment keeps your data private \ No newline at end of file diff --git a/packages/app-store/bigbluebutton/_metadata.ts b/packages/app-store/bigbluebutton/_metadata.ts new file mode 100644 index 00000000000000..49b52cab166d14 --- /dev/null +++ b/packages/app-store/bigbluebutton/_metadata.ts @@ -0,0 +1,30 @@ +import type { AppMeta } from "@calcom/types/App"; + +import _package from "./package.json"; + +export const metadata = { + name: "BigBlueButton", + description: _package.description, + type: "bigbluebutton_video", + variant: "conferencing", + categories: ["conferencing"], + logo: "icon.svg", + publisher: "Cal.com", + url: "https://bigbluebutton.org/", + slug: "bigbluebutton", + title: "BigBlueButton", + isGlobal: false, + email: "help@cal.com", + appData: { + location: { + linkType: "dynamic", + type: "integrations:bigbluebutton", + label: "BigBlueButton", + }, + }, + dirName: "bigbluebutton", + concurrentMeetings: true, + isOAuth: false, +} as AppMeta; + +export default metadata; \ No newline at end of file diff --git a/packages/app-store/bigbluebutton/api/add.ts b/packages/app-store/bigbluebutton/api/add.ts new file mode 100644 index 00000000000000..085d59b07be205 --- /dev/null +++ b/packages/app-store/bigbluebutton/api/add.ts @@ -0,0 +1,114 @@ +import type { NextApiRequest, NextApiResponse } from "next"; +import { throwIfNotHaveAdminAccessToTeam } from "@calcom/app-store/_utils/throwIfNotHaveAdminAccessToTeam"; +import { getServerErrorFromUnknown } from "@calcom/lib/server/getServerErrorFromUnknown"; +import prisma from "@calcom/prisma"; +import getInstalledAppPath from "../../_utils/getInstalledAppPath"; +import { BBBApi } from "../lib/bbb-api"; + +/** + * BigBlueButton app installation endpoint + * Validates BBB server connection before saving credentials + */ +export default async function handler(req: NextApiRequest, res: NextApiResponse) { + if (!req.session?.user?.id) { + return res.status(401).json({ message: "You must be logged in to do this" }); + } + + if (req.method !== "POST") { + return res.status(405).json({ message: "Method not allowed" }); + } + + const { teamId, returnTo } = req.query; + const { serverUrl, sharedSecret } = req.body; + + // Validate required fields + if (!serverUrl || !sharedSecret) { + return res.status(400).json({ + message: "Server URL and Shared Secret are required" + }); + } + + // Validate server URL format + try { + const url = new URL(serverUrl); + if (!url.protocol.startsWith("http")) { + throw new Error("Invalid protocol"); + } + } catch { + return res.status(400).json({ + message: "Invalid server URL format. Please provide a valid HTTP or HTTPS URL." + }); + } + + 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"; + + try { + // Test BBB server connection first before attempting to install + const bbbApi = new BBBApi({ serverUrl, sharedSecret }); + const isConnected = await bbbApi.testConnection(); + + if (!isConnected) { + return res.status(400).json({ + message: "Could not connect to BigBlueButton server. Please verify the server URL and shared secret." + }); + } + + // Use a transaction to prevent race conditions + const installation = await prisma.$transaction(async (tx) => { + // Check for existing installation within transaction + const existing = await tx.credential.findFirst({ + where: { + type: appType, + ...installForObject, + }, + }); + + if (existing) { + // Update existing installation instead of creating duplicate + return await tx.credential.update({ + where: { id: existing.id }, + data: { + key: { + serverUrl: serverUrl.replace(/\/+$/, ""), // Remove trailing slashes + sharedSecret, + }, + }, + }); + } + + // Create new installation if none exists + return await tx.credential.create({ + data: { + type: appType, + key: { + serverUrl: serverUrl.replace(/\/+$/, ""), // Remove trailing slashes + sharedSecret, + }, + ...installForObject, + appId: "bigbluebutton", + }, + }); + }); + + if (!installation) { + throw new Error("Unable to create BigBlueButton credential"); + } + + return res.status(200).json({ + url: returnTo ?? getInstalledAppPath({ variant: "conferencing", slug: "bigbluebutton" }), + message: "BigBlueButton successfully configured!" + }); + + } catch (error: unknown) { + // Log safe error message without sensitive credentials + console.error("BigBlueButton installation error:", error instanceof Error ? error.message : "Unknown error"); + const httpError = getServerErrorFromUnknown(error); + return res.status(httpError.statusCode).json({ message: httpError.message }); + } +} \ No newline at end of file diff --git a/packages/app-store/bigbluebutton/api/index.ts b/packages/app-store/bigbluebutton/api/index.ts new file mode 100644 index 00000000000000..9320291f2764dd --- /dev/null +++ b/packages/app-store/bigbluebutton/api/index.ts @@ -0,0 +1 @@ +export { default as add } from "./add"; \ No newline at end of file diff --git a/packages/app-store/bigbluebutton/index.ts b/packages/app-store/bigbluebutton/index.ts new file mode 100644 index 00000000000000..02c2de5f5cc282 --- /dev/null +++ b/packages/app-store/bigbluebutton/index.ts @@ -0,0 +1,3 @@ +export * as lib from "./lib"; +export * as api from "./api"; +export { metadata } from "./_metadata"; \ No newline at end of file diff --git a/packages/app-store/bigbluebutton/lib/VideoApiAdapter.ts b/packages/app-store/bigbluebutton/lib/VideoApiAdapter.ts new file mode 100644 index 00000000000000..55d945131c2f64 --- /dev/null +++ b/packages/app-store/bigbluebutton/lib/VideoApiAdapter.ts @@ -0,0 +1,175 @@ +import { v4 as uuidv4 } from "uuid"; +import type { CalendarEvent } from "@calcom/types/Calendar"; +import type { PartialReference } from "@calcom/types/EventManager"; +import type { VideoApiAdapter, VideoCallData } from "@calcom/types/VideoApiAdapter"; + +import getAppKeysFromSlug from "../../_utils/getAppKeysFromSlug"; +import { metadata } from "../_metadata"; +import { BBBApi } from "./bbb-api"; + +const BigBlueButtonVideoApiAdapter = (): VideoApiAdapter => { + return { + getAvailability: () => { + return Promise.resolve([]); + }, + + createMeeting: async (eventData: CalendarEvent): Promise => { + const appKeys = await getAppKeysFromSlug(metadata.slug); + + const serverUrl = appKeys.serverUrl as string; + const sharedSecret = appKeys.sharedSecret as string; + + if (!serverUrl || !sharedSecret) { + throw new Error("BigBlueButton configuration missing. Please configure server URL and shared secret."); + } + + // Generate unique meeting ID and passwords + const meetingID = `cal-${uuidv4()}`; + const moderatorPassword = uuidv4().substring(0, 8); + const attendeePassword = uuidv4().substring(0, 8); + + // Create meeting name + const meetingName = eventData.title || "Cal.com Meeting"; + + try { + const bbbApi = new BBBApi({ serverUrl, sharedSecret }); + + // Create the meeting + await bbbApi.createMeeting({ + meetingID, + name: meetingName, + moderatorPW: moderatorPassword, + attendeePW: attendeePassword, + welcome: `Welcome to ${meetingName}! Please wait for the host to start the meeting.`, + maxParticipants: 50, + }); + + // Generate attendee join URL as the canonical meeting URL (safe for sharing) + const attendeeJoinUrl = bbbApi.getJoinUrl({ + meetingID, + fullName: "Participant", + password: attendeePassword, + userID: "participant", + role: "viewer", + }); + + return { + type: metadata.type, + id: `${meetingID}|${moderatorPassword}|${serverUrl}`, // Store structured meeting data + password: attendeePassword, // Standard semantic: password for meeting access + url: attendeeJoinUrl, // Safe attendee URL as canonical URL + }; + } catch (error) { + console.error("Failed to create BigBlueButton meeting:", error); + throw new Error( + error instanceof Error + ? `BigBlueButton meeting creation failed: ${error.message}` + : "Failed to create BigBlueButton meeting" + ); + } + }, + + deleteMeeting: async (bookingRef: PartialReference): Promise => { + if (!bookingRef.meetingId) { + return; + } + + try { + // Parse structured meeting ID: meetingID|moderatorPassword|serverUrl + const idParts = bookingRef.meetingId.split("|"); + if (idParts.length !== 3) { + console.warn("Invalid BigBlueButton meeting ID format for deletion"); + return; + } + + const [actualMeetingId, moderatorPassword, storedServerUrl] = idParts; + + // Get app keys for shared secret (server URL comes from meeting ID) + const appKeys = await getAppKeysFromSlug(metadata.slug); + const sharedSecret = appKeys.sharedSecret as string; + + if (!sharedSecret) { + console.warn("BigBlueButton shared secret missing for meeting deletion"); + return; + } + + const bbbApi = new BBBApi({ + serverUrl: storedServerUrl, + sharedSecret + }); + + await bbbApi.endMeeting(actualMeetingId, moderatorPassword); + } catch (error) { + console.error("Failed to end BigBlueButton meeting:", error); + // Don't throw error for deletion failures to avoid blocking booking cancellation + } + }, + + updateMeeting: (bookingRef: PartialReference): Promise => { + // For BBB, we just return the existing meeting info + // BBB meetings are stateless and don't need updating + + const meetingId = bookingRef.meetingId as string; + const attendeePassword = bookingRef.meetingPassword as string; + const meetingUrl = bookingRef.meetingUrl as string; + + return Promise.resolve({ + type: metadata.type, + id: meetingId, + password: attendeePassword, // Standard attendee password + url: meetingUrl, + }); + }, + + /** + * Generate attendee join URL for guests + */ + getGuestJoinUrl: async ( + structuredMeetingId: string, + guestName: string, + attendeePassword: string + ): Promise => { + // Parse structured meeting ID: meetingID|moderatorPassword|serverUrl + const [actualMeetingId, , serverUrl] = structuredMeetingId.split("|"); + + const appKeys = await getAppKeysFromSlug(metadata.slug); + const sharedSecret = appKeys.sharedSecret as string; + + const bbbApi = new BBBApi({ serverUrl, sharedSecret }); + + return bbbApi.getJoinUrl({ + meetingID: actualMeetingId, + fullName: guestName || "Guest", + password: attendeePassword, + userID: `guest-${Date.now()}`, + role: "viewer", + }); + }, + + /** + * Generate moderator join URL for organizers + */ + getModeratorJoinUrl: async ( + structuredMeetingId: string, + organizerName: string + ): Promise => { + // Parse structured meeting ID: meetingID|moderatorPassword|serverUrl + const [actualMeetingId, moderatorPassword, serverUrl] = structuredMeetingId.split("|"); + + const appKeys = await getAppKeysFromSlug(metadata.slug); + const sharedSecret = appKeys.sharedSecret as string; + + const bbbApi = new BBBApi({ serverUrl, sharedSecret }); + + return bbbApi.getJoinUrl({ + meetingID: actualMeetingId, + fullName: organizerName || "Host", + password: moderatorPassword, + userID: "moderator", + role: "moderator", + }); + }, + }; +}; + +export default BigBlueButtonVideoApiAdapter; \ No newline at end of file diff --git a/packages/app-store/bigbluebutton/lib/bbb-api.ts b/packages/app-store/bigbluebutton/lib/bbb-api.ts new file mode 100644 index 00000000000000..742d0c49ccc9b0 --- /dev/null +++ b/packages/app-store/bigbluebutton/lib/bbb-api.ts @@ -0,0 +1,177 @@ +import crypto from "crypto"; +import axios from "axios"; + +interface BBBCredentials { + serverUrl: string; + sharedSecret: string; +} + +interface BBBMeetingParams { + meetingID: string; + name: string; + moderatorPW: string; + attendeePW: string; + welcome?: string; + logoutURL?: string; + maxParticipants?: number; +} + +interface BBBJoinParams { + meetingID: string; + fullName: string; + password: string; + userID?: string; + role?: "moderator" | "viewer"; +} + +export class BBBApi { + private serverUrl: string; + private sharedSecret: string; + + constructor(credentials: BBBCredentials) { + // Ensure server URL doesn't end with slash + this.serverUrl = credentials.serverUrl.replace(/\/+$/, ""); + this.sharedSecret = credentials.sharedSecret; + } + + /** + * Generate SHA-256 checksum for BBB API authentication + */ + private generateChecksum(queryString: string): string { + const checksumString = queryString + this.sharedSecret; + return crypto.createHash("sha256").update(checksumString).digest("hex"); + } + + /** + * Build authenticated BBB API URL + */ + private buildApiUrl(apiCall: string, params: Record): string { + // Remove undefined/null values and convert to string + const cleanParams = Object.entries(params) + .filter(([_, value]) => value !== undefined && value !== null) + .reduce((acc, [key, value]) => { + acc[key] = String(value); + return acc; + }, {} as Record); + + // Build query string + const queryString = new URLSearchParams(cleanParams).toString(); + + // Generate checksum + const checksum = this.generateChecksum(apiCall + queryString); + + // Build final URL + return `${this.serverUrl}/bigbluebutton/api/${apiCall}?${queryString}&checksum=${checksum}`; + } + + /** + * Create a BigBlueButton meeting + */ + async createMeeting(params: BBBMeetingParams): Promise { + const apiParams = { + meetingID: params.meetingID, + name: params.name, + moderatorPW: params.moderatorPW, + attendeePW: params.attendeePW, + welcome: params.welcome || `Welcome to ${params.name}!`, + logoutURL: params.logoutURL, + maxParticipants: params.maxParticipants || 100, + autoStartRecording: false, + allowStartStopRecording: false, + record: false, + duration: 0, // No time limit + }; + + const url = this.buildApiUrl("create", apiParams); + + try { + const response = await axios.get(url, { + timeout: 10000, // 10 second timeout + }); + + // BBB returns XML, check for errors + if (response.data.includes("FAILED")) { + const messageMatch = response.data.match(/(.*?)<\/message>/); + const message = messageMatch ? messageMatch[1] : "Failed to create meeting"; + throw new Error(`BBB API Error: ${message}`); + } + } catch (error) { + if (error instanceof Error) { + throw new Error(`Failed to create BBB meeting: ${error.message}`); + } + throw new Error("Failed to create BBB meeting"); + } + } + + /** + * Generate join URL for a participant + */ + getJoinUrl(params: BBBJoinParams): string { + const apiParams = { + meetingID: params.meetingID, + fullName: params.fullName, + password: params.password, + userID: params.userID, + redirect: "true", // Redirect immediately to meeting + }; + + return this.buildApiUrl("join", apiParams); + } + + /** + * End a BigBlueButton meeting + */ + async endMeeting(meetingID: string, moderatorPassword: string): Promise { + const apiParams = { + meetingID, + password: moderatorPassword, + }; + + const url = this.buildApiUrl("end", apiParams); + + try { + const response = await axios.get(url, { + timeout: 10000, + }); + + if (response.data.includes("FAILED")) { + const messageMatch = response.data.match(/(.*?)<\/message>/); + const message = messageMatch ? messageMatch[1] : "Failed to end meeting"; + throw new Error(`BBB API Error: ${message}`); + } + } catch (error) { + if (error instanceof Error) { + throw new Error(`Failed to end BBB meeting: ${error.message}`); + } + throw new Error("Failed to end BBB meeting"); + } + } + + /** + * Test connection to BBB server and validate authentication + * Uses getMeetings which requires a valid checksum (shared secret) + * If auth fails, BBB returns FAILED with specific error message + */ + async testConnection(): Promise { + try { + const url = this.buildApiUrl("getMeetings", {}); + const response = await axios.get(url, { + timeout: 5000, + }); + + // Check response status and content + if (response.status !== 200) return false; + + // BBB returns FAILED for auth errors, SUCCESS for valid auth (even empty meetings) + if (response.data.includes("FAILED")) { + return false; + } + + // Valid auth should include returncode SUCCESS or valid XML structure + return response.data.includes("SUCCESS") || + response.data.includes(""); + } catch (error) { + return false; + } + } +} \ No newline at end of file diff --git a/packages/app-store/bigbluebutton/lib/index.ts b/packages/app-store/bigbluebutton/lib/index.ts new file mode 100644 index 00000000000000..fa4541d1fa4536 --- /dev/null +++ b/packages/app-store/bigbluebutton/lib/index.ts @@ -0,0 +1,2 @@ +export { default as VideoApiAdapter } from "./VideoApiAdapter"; +export { BBBApi } from "./bbb-api"; \ No newline at end of file diff --git a/packages/app-store/bigbluebutton/package.json b/packages/app-store/bigbluebutton/package.json new file mode 100644 index 00000000000000..f63311634080db --- /dev/null +++ b/packages/app-store/bigbluebutton/package.json @@ -0,0 +1,13 @@ +{ + "private": true, + "name": "@calcom/bigbluebutton", + "version": "0.0.0", + "main": "./index.ts", + "description": "BigBlueButton is an open-source video conferencing system for education and business.", + "dependencies": { + "@calcom/lib": "workspace:*" + }, + "devDependencies": { + "@calcom/types": "workspace:*" + } +} \ No newline at end of file diff --git a/packages/app-store/bigbluebutton/static/icon.svg b/packages/app-store/bigbluebutton/static/icon.svg new file mode 100644 index 00000000000000..27c9a0fca63ecd --- /dev/null +++ b/packages/app-store/bigbluebutton/static/icon.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + BBB + \ No newline at end of file diff --git a/packages/app-store/bigbluebutton/zod.ts b/packages/app-store/bigbluebutton/zod.ts new file mode 100644 index 00000000000000..86b161dee9880f --- /dev/null +++ b/packages/app-store/bigbluebutton/zod.ts @@ -0,0 +1,11 @@ +import { z } from "zod"; + +export const appKeysSchema = z.object({ + serverUrl: z.string().url("Please provide a valid server URL"), + sharedSecret: z.string().trim().min(8, "Shared secret must be at least 8 characters").refine( + (value) => value.trim() === value && value.length > 0, + "Shared secret cannot be empty or contain only whitespace" + ), +}); + +export const appDataSchema = z.object({}); \ No newline at end of file diff --git a/packages/app-store/video.adapters.generated.ts b/packages/app-store/video.adapters.generated.ts index f1e0f05ffe7f78..74252cdcac0a37 100644 --- a/packages/app-store/video.adapters.generated.ts +++ b/packages/app-store/video.adapters.generated.ts @@ -10,6 +10,7 @@ export const VideoApiAdapterMap = huddle01video: import("./huddle01video/lib/VideoApiAdapter"), jelly: import("./jelly/lib/VideoApiAdapter"), jitsivideo: import("./jitsivideo/lib/VideoApiAdapter"), + bigbluebutton: import("./bigbluebutton/lib/VideoApiAdapter"), lyra: import("./lyra/lib/VideoApiAdapter"), nextcloudtalk: import("./nextcloudtalk/lib/VideoApiAdapter"), office365video: import("./office365video/lib/VideoApiAdapter"),