Files
lck-control-backend/src/routes/chat/websocket.ts
omigamedev cc8ab2320b YouTube/Twitch live chat backend WebSocket proxy
- YouTube chat polling via liveBroadcasts + liveChat/messages APIs
- Twitch IRC WebSocket client with IRCv3 tag parsing
- ChatManager orchestrator with token refresh, retry logic
- WebSocket endpoint at /chat/ws with JWT auth
- Added chat:read, chat:edit to Twitch OAuth scopes
2026-03-01 22:19:19 +01:00

89 lines
2.7 KiB
TypeScript

import { FastifyPluginAsync } from 'fastify';
import { verifyAccessToken } from '../../plugins/auth.js';
import { ChatManager } from '../../services/chat-manager.service.js';
interface ChatWsMessage {
type: 'subscribe' | 'unsubscribe' | 'send_message';
planId?: string;
destinationId?: string;
text?: string;
}
export function createChatRoutes(chatManager: ChatManager): FastifyPluginAsync {
const chatRoutes: FastifyPluginAsync = async (fastify) => {
fastify.get('/chat/ws', { websocket: true }, async (socket, request) => {
// Authenticate via query param
const url = new URL(request.url, `http://${request.headers.host}`);
const token = url.searchParams.get('token');
if (!token) {
socket.send(JSON.stringify({ type: 'error', error: 'Missing token' }));
socket.close();
return;
}
let userId: string;
try {
userId = await verifyAccessToken(token);
} catch {
socket.send(JSON.stringify({ type: 'error', error: 'Invalid or expired token' }));
socket.close();
return;
}
request.log.info({ userId }, 'Chat WebSocket connected');
socket.on('message', async (data: Buffer) => {
try {
const raw = data.toString();
request.log.info({ userId, raw }, 'Chat WS message received');
const msg: ChatWsMessage = JSON.parse(raw);
switch (msg.type) {
case 'subscribe':
if (msg.planId) {
await chatManager.startChat(msg.planId, userId, socket);
}
break;
case 'unsubscribe':
if (msg.planId) {
await chatManager.stopChat(msg.planId, userId);
}
break;
case 'send_message':
if (msg.planId && msg.destinationId && msg.text) {
await chatManager.handleSendMessage(
msg.planId,
userId,
msg.destinationId,
msg.text,
);
}
break;
default:
socket.send(JSON.stringify({ type: 'error', error: `Unknown message type: ${msg.type}` }));
}
} catch (err) {
request.log.error({ err }, 'Chat WebSocket message handling error');
socket.send(JSON.stringify({ type: 'error', error: 'Internal error' }));
}
});
socket.on('close', () => {
request.log.info({ userId }, 'Chat WebSocket disconnected');
chatManager.stopAllForSocket(socket);
});
socket.on('error', (err: Error) => {
request.log.error({ err, userId }, 'Chat WebSocket error');
chatManager.stopAllForSocket(socket);
});
});
};
return chatRoutes;
}