Add ban/unban functionality with BansPanel in space settings, voice moderation context menu (mute/deafen/disconnect), and fix federated space settings panels to use origin-aware API client. Show domain indicators for federated members in MembersPanel.
86 lines
3.3 KiB
TypeScript
86 lines
3.3 KiB
TypeScript
import type { FastifyInstance } from 'fastify';
|
|
import { AccessToken, TrackSource } from 'livekit-server-sdk';
|
|
import { authenticate } from '../utils/auth.js';
|
|
import { config } from '../config.js';
|
|
import { getChannelSpaceId, hasPermission, computePermissions, isDmMember, PermissionBits } from '../utils/permissions.js';
|
|
import type { LiveKitTokenRequest, LiveKitTokenResponse } from '@backspace/shared';
|
|
|
|
export async function livekitRoutes(app: FastifyInstance): Promise<void> {
|
|
app.post<{ Body: LiveKitTokenRequest & { dmChannelId?: string } }>('/api/livekit/token', {
|
|
preHandler: authenticate,
|
|
}, async (request, reply) => {
|
|
if (!config.livekit.apiKey || !config.livekit.apiSecret) {
|
|
return reply.code(503).send({ error: 'Voice/video is not configured on this server', statusCode: 503 });
|
|
}
|
|
|
|
const { channelId, dmChannelId } = request.body as { channelId?: string; dmChannelId?: string };
|
|
|
|
// Determine room name based on channel type
|
|
let roomName: string;
|
|
|
|
// Default: full publish (DM calls always get full permissions)
|
|
let canSpeak = true;
|
|
let canStream = true;
|
|
|
|
if (dmChannelId && typeof dmChannelId === 'string') {
|
|
// DM call token
|
|
if (!isDmMember(dmChannelId, request.userId)) {
|
|
return reply.code(403).send({ error: 'You are not a member of this DM channel', statusCode: 403 });
|
|
}
|
|
roomName = `dm-${dmChannelId}`;
|
|
} else if (channelId && typeof channelId === 'string') {
|
|
// Space voice channel token
|
|
const spaceId = getChannelSpaceId(channelId);
|
|
if (!spaceId) {
|
|
return reply.code(404).send({ error: 'Channel not found', statusCode: 404 });
|
|
}
|
|
if (!hasPermission(request.userId, spaceId, PermissionBits.CONNECT, channelId)) {
|
|
return reply.code(403).send({ error: 'Missing CONNECT permission', statusCode: 403 });
|
|
}
|
|
// Check SPEAK and STREAM permissions for granular token grants
|
|
const perms = computePermissions(request.userId, spaceId, channelId);
|
|
canSpeak = (perms & PermissionBits.SPEAK) !== 0n || (perms & PermissionBits.ADMINISTRATOR) !== 0n;
|
|
canStream = (perms & PermissionBits.STREAM) !== 0n || (perms & PermissionBits.ADMINISTRATOR) !== 0n;
|
|
roomName = channelId;
|
|
} else {
|
|
return reply.code(400).send({ error: 'channelId or dmChannelId is required', statusCode: 400 });
|
|
}
|
|
|
|
const identity = `${request.userId}:${request.username}`;
|
|
|
|
const token = new AccessToken(config.livekit.apiKey, config.livekit.apiSecret, {
|
|
identity,
|
|
ttl: '1h',
|
|
});
|
|
|
|
// Build canPublishSources based on permissions
|
|
const canPublishSources: TrackSource[] = [];
|
|
if (canSpeak) {
|
|
canPublishSources.push(TrackSource.MICROPHONE);
|
|
canPublishSources.push(TrackSource.CAMERA);
|
|
}
|
|
if (canStream) {
|
|
canPublishSources.push(TrackSource.SCREEN_SHARE, TrackSource.SCREEN_SHARE_AUDIO);
|
|
}
|
|
|
|
token.addGrant({
|
|
room: roomName,
|
|
roomJoin: true,
|
|
canPublish: canSpeak || canStream,
|
|
canPublishSources,
|
|
canSubscribe: true,
|
|
canPublishData: true,
|
|
});
|
|
|
|
const jwt = await token.toJwt();
|
|
|
|
const livekitUrl = config.livekit.url ?? '';
|
|
|
|
const response: LiveKitTokenResponse = {
|
|
token: jwt,
|
|
url: livekitUrl
|
|
};
|
|
return reply.code(200).send(response);
|
|
});
|
|
}
|