// /api/rooms — list/create rooms, manage members, fetch state. import { Router } from 'express'; import { requireUser } from '../auth.js'; import { listRoomsForUser, getRoomBySlug, createRoom, addMember, removeMember, listMembers, isMember, getRoomState, ensurePersonalRoom } from '../rooms.js'; import { getStation } from '../stations.js'; import { dispatchRoomCommand, hasDisplay } from '../ws.js'; export const router = Router(); router.use(requireUser); router.get('/', (req, res) => { // Guarantee a personal room exists for every authenticated user. ensurePersonalRoom(req.user); res.json(listRoomsForUser(req.user.id)); }); router.post('/', (req, res) => { const { name, slug } = req.body || {}; if (!name || typeof name !== 'string') return res.status(400).json({ error: 'name required' }); const room = createRoom({ name: name.trim(), slug: slug?.trim() || undefined, ownerId: req.user.id }); res.status(201).json(room); }); router.get('/:slug', (req, res) => { const room = getRoomBySlug(req.params.slug); if (!room) return res.status(404).json({ error: 'not found' }); if (!isMember(room.id, req.user.id)) return res.status(403).json({ error: 'not a member' }); const state = getRoomState(room.id); const station = state.station_id ? getStation(state.station_id) : null; res.json({ ...room, state: { ...state, station } }); }); router.get('/:slug/members', (req, res) => { const room = getRoomBySlug(req.params.slug); if (!room) return res.status(404).json({ error: 'not found' }); if (!isMember(room.id, req.user.id)) return res.status(403).json({ error: 'not a member' }); res.json(listMembers(room.id)); }); router.post('/:slug/members', (req, res) => { const room = getRoomBySlug(req.params.slug); if (!room) return res.status(404).json({ error: 'not found' }); if (!isMember(room.id, req.user.id)) return res.status(403).json({ error: 'not a member' }); const userId = Number(req.body?.user_id); if (!userId) return res.status(400).json({ error: 'user_id required' }); const role = req.body?.role === 'guest' ? 'guest' : 'member'; addMember(room.id, userId, role); res.json(listMembers(room.id)); }); router.delete('/:slug/members/:userId', (req, res) => { const room = getRoomBySlug(req.params.slug); if (!room) return res.status(404).json({ error: 'not found' }); if (!isMember(room.id, req.user.id)) return res.status(403).json({ error: 'not a member' }); removeMember(room.id, Number(req.params.userId)); res.json(listMembers(room.id)); }); // Inject a playback intent for the room from outside the WS hub. Used by // smart-home/scripts/integrations: the request behaves like a synthetic // controller — the master (display) actually plays the audio and emits its // authoritative `state`, which the server rebroadcasts so every other peer // stays in sync. 409 when no display is connected, because without a master // there's no audio source to honour the intent. router.post('/:slug/command', (req, res) => { const room = getRoomBySlug(req.params.slug); if (!room) return res.status(404).json({ error: 'not found' }); if (!isMember(room.id, req.user.id)) return res.status(403).json({ error: 'not a member' }); const action = String(req.body?.action || ''); if (!['play', 'pause', 'stop', 'volume'].includes(action)) { return res.status(400).json({ error: 'action must be one of play|pause|stop|volume' }); } const msg = { type: 'command', action }; if (action === 'play') { const stationId = Number(req.body?.stationId); if (!Number.isFinite(stationId)) return res.status(400).json({ error: 'stationId required for play' }); msg.stationId = stationId; } else if (action === 'volume') { const value = Number(req.body?.value); if (!Number.isFinite(value)) return res.status(400).json({ error: 'numeric value required for volume' }); msg.value = Math.max(0, Math.min(1, value)); } if (!hasDisplay(room.slug)) { return res.status(409).json({ error: 'no display connected to room', slug: room.slug }); } const result = dispatchRoomCommand(room, msg); if (!result) return res.status(400).json({ error: 'command rejected' }); res.json({ ok: true, room: { id: room.id, slug: room.slug, name: room.name }, state: { ...result.state, station: result.station } }); });