Files
chad/server/routes/channels.ts
2026-04-12 22:35:47 +06:00

256 lines
6.8 KiB
TypeScript

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 })
}
}
})
}