Files
DiscordClone/convex/voice.ts
Bryan1029384756 b7a4cf4ce8
All checks were successful
Build and Release / build-and-release (push) Successful in 13m12s
feat(ui): add Button, Modal, Spinner, Toast, and Tooltip components with styles
- Implemented Button component with various props for customization.
- Created Modal component with header, content, and footer subcomponents.
- Added Spinner component for loading indicators.
- Developed Toast component for displaying notifications.
- Introduced Tooltip component for contextual hints with keyboard shortcuts.
- Added corresponding CSS modules for styling each component.
- Updated index file to export new components.
- Configured TypeScript settings for the UI package.
2026-04-14 09:02:14 -05:00

92 lines
3.3 KiB
TypeScript

"use node";
import { action } from "./_generated/server";
import { v } from "convex/values";
import { AccessToken, RoomServiceClient } from "livekit-server-sdk";
/**
* Generate a LiveKit join token for a voice channel.
*
* LiveKit servers run with `room.auto_create: false` reject joins for
* rooms that don't already exist — the client gets a 404 "requested
* room does not exist" back from the /rtc/v1/validate endpoint. To
* make this deployment-agnostic, we pre-create the room via the
* LiveKit Server SDK before minting the token. When auto-create is
* enabled the `createRoom` call is idempotent (409 Conflict is
* swallowed silently), so the same code path works on both
* configurations.
*
* Requires `LIVEKIT_URL` (or the frontend's `VITE_LIVEKIT_URL` as a
* fallback) in the Convex environment so the RoomServiceClient
* can talk to the LiveKit API.
*/
export const getToken = action({
args: {
channelId: v.string(),
userId: v.string(),
username: v.string(),
},
returns: v.object({ token: v.string() }),
handler: async (_ctx, args) => {
const apiKey = process.env.LIVEKIT_API_KEY || "devkey";
const apiSecret = process.env.LIVEKIT_API_SECRET || "secret";
const livekitUrl =
process.env.LIVEKIT_URL || process.env.VITE_LIVEKIT_URL || "";
// Ensure the room exists. The LiveKit API accepts `http(s)` URLs
// for the management endpoint, but the frontend connect URL is a
// `wss://` — swap the scheme when needed.
if (livekitUrl) {
const httpUrl = livekitUrl
.replace(/^wss:\/\//i, "https://")
.replace(/^ws:\/\//i, "http://");
try {
const roomService = new RoomServiceClient(httpUrl, apiKey, apiSecret);
await roomService.createRoom({
name: args.channelId,
// Empty rooms auto-destroy after 5 minutes with no participants,
// matching LiveKit's own default so stale rooms from a crashed
// client don't pile up forever.
emptyTimeout: 5 * 60,
// 50 participants is plenty for a voice channel in this
// single-server deployment and keeps any runaway join loop
// from hitting the global limit.
maxParticipants: 50,
});
} catch (err: any) {
// 409 / "already exists" is expected when a room has already
// been created by an earlier join — swallow it and continue.
const message = String(err?.message ?? err ?? "");
const status = err?.status ?? err?.statusCode;
const alreadyExists =
status === 409 ||
/already exists/i.test(message) ||
/AlreadyExists/i.test(message);
if (!alreadyExists) {
// Non-fatal: log and fall through to token generation. If the
// real issue was misconfiguration the client will surface the
// 404 it already does.
console.warn("LiveKit createRoom failed:", message);
}
}
}
const at = new AccessToken(apiKey, apiSecret, {
identity: args.userId,
name: args.username,
ttl: "24h",
});
at.addGrant({
roomJoin: true,
room: args.channelId,
canPublish: true,
canSubscribe: true,
canPublishData: true,
});
const token = await at.toJwt();
return { token };
},
});