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
This commit is contained in:
88
src/routes/chat/websocket.ts
Normal file
88
src/routes/chat/websocket.ts
Normal file
@@ -0,0 +1,88 @@
|
||||
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;
|
||||
}
|
||||
Reference in New Issue
Block a user