import type { FastifyInstance } from 'fastify' import { z } from 'zod' import prisma from '../prisma/client.ts' import { channelPublicSelect } from '../dto/channel.dto.ts' export default function (fastify: FastifyInstance) { // GET /chad/channels - List all channels with client counts fastify.get('/channels', async (req, reply) => { if (!req.user) { return reply.code(401).send({ error: 'Unauthorized' }) } const channels = await prisma.channel.findMany({ select: channelPublicSelect, orderBy: { name: 'asc' }, }) // Add client count to each channel using Socket.IO rooms const channelsWithCounts = await Promise.all( channels.map(async channel => { const socketsInChannel = await fastify.io.in(channel.id).fetchSockets() return { ...channel, clientCount: socketsInChannel.length, } }), ) return channelsWithCounts }) // GET /chad/channels/:id - Get specific channel with client list fastify.get('/channels/:id', async (req, reply) => { if (!req.user) { return reply.code(401).send({ error: 'Unauthorized' }) } try { const paramsSchema = z.object({ id: z.string(), }) const params = paramsSchema.parse(req.params) const channel = await prisma.channel.findUnique({ where: { id: params.id }, select: channelPublicSelect, }) if (!channel) { return reply.code(404).send({ error: 'Channel not found' }) } // Get clients in this channel using Socket.IO rooms const sockets = await fastify.io.in(params.id).fetchSockets() const clients = sockets .filter(s => s.data.joined) .map(s => { const channelId = Array.from(s.rooms).find(room => room !== s.id) || 'default' return { socketId: s.id, userId: s.data.userId, username: s.data.username, displayName: s.data.displayName, inputMuted: s.data.inputMuted, outputMuted: s.data.outputMuted, currentChannelId: channelId, } }) return { ...channel, clients, } } catch (err) { fastify.log.error(err) reply.code(400) if (err instanceof z.ZodError) { reply.send({ error: z.prettifyError(err) }) } else { reply.send({ error: err.message }) } } }) // POST /chad/channels - Create channel fastify.post('/channels', async (req, reply) => { if (!req.user) { return reply.code(401).send({ error: 'Unauthorized' }) } try { const schema = z.object({ name: z.string().min(1).max(50), maxClients: z.number().int().positive().optional().nullable(), persistent: z.boolean().default(false), }) const input = schema.parse(req.body) const channel = await prisma.channel.create({ data: { name: input.name, maxClients: input.maxClients, persistent: input.persistent, owner_id: req.user.id, }, select: channelPublicSelect, }) // Notify all connected clients about new channel fastify.io.emit('channelCreated', channel) return channel } catch (err) { fastify.log.error(err) reply.code(400) if (err instanceof z.ZodError) { reply.send({ error: z.prettifyError(err) }) } else { reply.send({ error: err.message }) } } }) // PATCH /chad/channels/:id - Update channel fastify.patch('/channels/:id', async (req, reply) => { if (!req.user) { return reply.code(401).send({ error: 'Unauthorized' }) } try { const paramsSchema = z.object({ id: z.string(), }) const params = paramsSchema.parse(req.params) const schema = z.object({ name: z.string().min(1).max(50).optional(), maxClients: z.number().int().positive().optional().nullable(), }) const input = schema.parse(req.body) // Cannot update default channel if (params.id === 'default') { return reply.code(403).send({ error: 'Cannot modify default channel' }) } const existing = await prisma.channel.findUnique({ where: { id: params.id }, }) if (!existing) { return reply.code(404).send({ error: 'Channel not found' }) } if (existing.owner_id !== req.user.id) { return reply.code(403).send({ error: 'Not channel owner' }) } const channel = await prisma.channel.update({ where: { id: params.id }, data: input, select: channelPublicSelect, }) fastify.io.emit('channelUpdated', channel) return channel } catch (err) { fastify.log.error(err) reply.code(400) if (err instanceof z.ZodError) { reply.send({ error: z.prettifyError(err) }) } else { reply.send({ error: err.message }) } } }) // DELETE /chad/channels/:id - Delete channel fastify.delete('/channels/:id', async (req, reply) => { if (!req.user) { return reply.code(401).send({ error: 'Unauthorized' }) } try { const paramsSchema = z.object({ id: z.string(), }) const params = paramsSchema.parse(req.params) if (params.id === 'default') { return reply.code(403).send({ error: 'Cannot delete default channel' }) } const existing = await prisma.channel.findUnique({ where: { id: params.id }, }) if (!existing) { return reply.code(404).send({ error: 'Channel not found' }) } if (existing.owner_id !== req.user.id) { return reply.code(403).send({ error: 'Not channel owner' }) } // Move all users in this channel back to default using Socket.IO rooms const sockets = await fastify.io.in(params.id).fetchSockets() for (const socket of sockets) { // Close all their producers for (const producer of socket.data.producers.values()) { producer.close() } socket.data.producers.clear() // Close all their consumers for (const consumer of socket.data.consumers.values()) { consumer.close() } socket.data.consumers.clear() // Move to default room await socket.leave(params.id) await socket.join('default') socket.emit('forcedChannelSwitch', { channelId: 'default' }) } await prisma.channel.delete({ where: { id: params.id }, }) fastify.io.emit('channelDeleted', { channelId: params.id }) return { success: true } } catch (err) { fastify.log.error(err) reply.code(400) if (err instanceof z.ZodError) { reply.send({ error: z.prettifyError(err) }) } else { reply.send({ error: err.message }) } } }) }