diff --git a/package-lock.json b/package-lock.json index f7ef8c4..5a71cbf 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "passport-google-oauth20": "^2.0.0", "passport-jwt": "^4.0.1", "redis": "^4.6.14", + "socket.io": "^4.7.5", "winston": "^3.11.0", "youtube-chat": "^2.2.0" }, @@ -269,6 +270,12 @@ "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", "license": "BSD-3-Clause" }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "license": "MIT" + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -328,11 +335,16 @@ "@types/node": "*" } }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==", + "license": "MIT" + }, "node_modules/@types/cors": { "version": "2.8.17", "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", - "dev": true, "license": "MIT", "dependencies": { "@types/node": "*" @@ -419,7 +431,6 @@ "version": "20.14.12", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.12.tgz", "integrity": "sha512-r7wNXakLeSsGT0H1AU863vS2wa5wBOK4bWMjZz2wj+8nBx+m5PeIn0k8AloSLpRuiwdRQZwarZqHE4FNArPuJQ==", - "dev": true, "license": "MIT", "dependencies": { "undici-types": "~5.26.4" @@ -706,6 +717,15 @@ ], "license": "MIT" }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "license": "MIT", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, "node_modules/base64url": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/base64url/-/base64url-3.0.1.tgz", @@ -1155,6 +1175,68 @@ "node": ">= 0.8" } }, + "node_modules/engine.io": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.5.tgz", + "integrity": "sha512-C5Pn8Wk+1vKBoHghJODM63yk8MvrO9EWZUfkAt5HAqIgPE4/8FF0PEGHXtEd40l223+cE5ABWuPzm38PHFXfMA==", + "license": "MIT", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT" + }, "node_modules/es-define-property": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", @@ -2665,6 +2747,116 @@ "is-arrayish": "^0.3.1" } }, + "node_modules/socket.io": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.5.tgz", + "integrity": "sha512-DmeAkF6cwM9jSfmp6Dr/5/mfMwb5Z5qRrSXLpo3Fq5SqyU8CMF15jIN4ZhfSwu35ksM1qmHZDQ/DK5XTccSTvA==", + "license": "MIT", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "license": "MIT", + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT" + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "license": "MIT", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.6.tgz", + "integrity": "sha512-O/09Bd4Z1fBrU4VzkhFqVgpPzaGbw6Sm9FEkBT1A/YBXQFGuuSxa1dN2nxgxS34JmKXqYx8CZAwEVoJFImUXIg==", + "license": "MIT", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "license": "MIT" + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -2976,7 +3168,6 @@ "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true, "license": "MIT" }, "node_modules/unpipe": { @@ -3097,6 +3288,27 @@ "dev": true, "license": "ISC" }, + "node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", diff --git a/package.json b/package.json index 2cdfde8..1a67841 100644 --- a/package.json +++ b/package.json @@ -9,41 +9,42 @@ "prisma:generate": "prisma generate", "prisma:migrate": "prisma migrate deploy" }, - "dependencies": { - "@prisma/client": "^5.0.0", - "axios": "^1.6.0", - "compression": "^1.7.4", - "cors": "^2.8.5", - "dotenv": "^16.4.5", - "express": "^4.18.2", - "express-session": "^1.18.0", - "googleapis": "^130.0.0", - "helmet": "^7.1.0", - "joi": "^17.13.3", - "jsonwebtoken": "^9.0.0", - "node-cron": "^3.0.3", - "passport": "^0.7.0", - "passport-google-oauth20": "^2.0.0", - "passport-jwt": "^4.0.1", - "redis": "^4.6.14", - "winston": "^3.11.0", - "youtube-chat": "^2.2.0" - }, - "devDependencies": { - "@types/compression": "^1.7.5", - "@types/cors": "^2.8.17", - "@types/express": "^4.17.21", - "@types/express-session": "^1.18.0", - "@types/gapi": "^0.0.47", - "@types/gapi.auth2": "^0.0.60", - "@types/jsonwebtoken": "^9.0.5", - "@types/node": "^20.10.0", - "@types/node-cron": "^3.0.11", - "@types/passport": "^1.0.16", - "@types/passport-google-oauth20": "^2.0.14", - "@types/passport-jwt": "^4.0.1", - "prisma": "^5.0.0", - "ts-node-dev": "^2.0.0", - "typescript": "^5.3.0" - } - } \ No newline at end of file + "dependencies": { + "@prisma/client": "^5.0.0", + "axios": "^1.6.0", + "compression": "^1.7.4", + "cors": "^2.8.5", + "dotenv": "^16.4.5", + "express": "^4.18.2", + "express-session": "^1.18.0", + "googleapis": "^130.0.0", + "helmet": "^7.1.0", + "joi": "^17.13.3", + "jsonwebtoken": "^9.0.0", + "node-cron": "^3.0.3", + "passport": "^0.7.0", + "passport-google-oauth20": "^2.0.0", + "passport-jwt": "^4.0.1", + "redis": "^4.6.14", + "socket.io": "^4.7.5", + "winston": "^3.11.0", + "youtube-chat": "^2.2.0" + }, + "devDependencies": { + "@types/compression": "^1.7.5", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/express-session": "^1.18.0", + "@types/gapi": "^0.0.47", + "@types/gapi.auth2": "^0.0.60", + "@types/jsonwebtoken": "^9.0.5", + "@types/node": "^20.10.0", + "@types/node-cron": "^3.0.11", + "@types/passport": "^1.0.16", + "@types/passport-google-oauth20": "^2.0.14", + "@types/passport-jwt": "^4.0.1", + "prisma": "^5.0.0", + "ts-node-dev": "^2.0.0", + "typescript": "^5.3.0" + } +} diff --git a/src/app.ts b/src/app.ts index b22939e..4ed46e0 100644 --- a/src/app.ts +++ b/src/app.ts @@ -4,19 +4,22 @@ import cors from 'cors'; import helmet from 'helmet'; import compression from 'compression'; import session from 'express-session'; +import http from 'http'; +import { Server } from 'socket.io'; import { errorHandler } from './middlewares/errorHandler'; import authRoutes from './routes/auth'; -import chessRoutes from './routes/chess';// Import the test routes +import chessRoutes from './routes/chess'; +import { setupChatSocket } from './controllers/chatController'; import './config/passport'; +import {logger} from './utils/logger'; import './utils/scheduler'; -import chatRoutes from './routes/chat'; // Added import for chatRoutes const app = express(); // Configure CORS to allow requests from the frontend app.use(cors({ - origin: process.env.FRONTEND || 'wa', // Use the FRONTEND environment variable or default to localhost - credentials: true // Allow credentials (cookies, authorization headers, etc.) + origin: process.env.FRONTEND || 'http://localhost:5173', + credentials: true })); app.use(helmet()); @@ -26,18 +29,33 @@ app.use(express.urlencoded({ extended: true })); // Configure express-session app.use(session({ - secret: process.env.SESSION_SECRET || 'your_secret_key', // Use a strong secret key + secret: process.env.SESSION_SECRET || 'your_secret_key', resave: false, saveUninitialized: false, - cookie: { secure: process.env.NODE_ENV === 'production' } // Use secure cookies in production + cookie: { secure: process.env.NODE_ENV === 'production' } })); app.use(passport.initialize()); app.use(passport.session()); app.use('/api/auth', authRoutes); -app.use('/api/chess', chessRoutes);// Use the test routes -app.use('/api/chat', chatRoutes); // Use the imported chatRoutes +app.use('/api/chess', chessRoutes); app.use(errorHandler); -export default app; +// Create HTTP server and setup Socket.IO +const server = http.createServer(app); +const io = new Server(server, { + cors: { + origin: process.env.FRONTEND || 'http://localhost:5173', + methods: ["GET", "POST"] + } +}); + +server.listen(2999, () => { + logger.info(`Server is running on port ${2999}`); +}); + +// Setup chat socket connections +setupChatSocket(io); + +export default app; \ No newline at end of file diff --git a/src/controllers/chatController.ts b/src/controllers/chatController.ts index e5bc11b..59e00b5 100644 --- a/src/controllers/chatController.ts +++ b/src/controllers/chatController.ts @@ -1,93 +1,58 @@ -import { Request, Response, NextFunction } from 'express'; +import { Server, Socket } from 'socket.io'; import { LiveChat } from 'youtube-chat'; -export class ChatController { - private activeLiveChats: Map = new Map(); +const CHANNEL_ID = 'UCAov2BBv1ZJav0c_yHEciAw'; - public startLiveStreamChat = async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const { url } = req.query; - if (!url || typeof url !== 'string') { - res.status(400).json({ error: 'Invalid YouTube URL' }); - return; - } +const setupChatSocket = (io: Server) => { + io.on('connection', (socket: Socket) => { + console.log('New client connected'); - const liveId = this.extractLiveId(url); - if (!liveId) { - res.status(400).json({ error: 'Invalid YouTube URL' }); - return; - } + let liveChat: LiveChat | null = null; - if (!this.activeLiveChats.has(liveId)) { - const liveChat = new LiveChat({ liveId }); - this.activeLiveChats.set(liveId, liveChat); - await liveChat.start(); + const startLiveChat = async () => { + if (liveChat) { + await liveChat.stop(); } - res.status(200).json({ message: 'Live chat started', liveId }); - } catch (error) { - next(error); - } - }; - - public getLiveStreamMessages = async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const { liveId } = req.params; - const liveChat = this.activeLiveChats.get(liveId); + liveChat = new LiveChat({ channelId: CHANNEL_ID }); - if (!liveChat) { - res.status(404).json({ error: 'Live chat not found' }); - return; - } + liveChat.on('start', (liveId) => { + console.log(`LiveChat started for live stream: ${liveId}`); + socket.emit('chatStarted', { liveId }); + }); - res.writeHead(200, { - 'Content-Type': 'text/event-stream', - 'Cache-Control': 'no-cache', - 'Connection': 'keep-alive' + liveChat.on('end', () => { + console.log('LiveChat ended'); + socket.emit('chatEnded'); }); - const messageHandler = (chatItem: any) => { - res.write(`data: ${JSON.stringify(chatItem)}\n\n`); - }; + liveChat.on('chat', (chatItem) => { + socket.emit('chatMessage', chatItem); + }); - const errorHandler = (err: Error) => { + liveChat.on('error', (err) => { console.error('LiveChat error:', err); - res.write(`data: ${JSON.stringify({ error: 'An error occurred' })}\n\n`); - }; - - liveChat.on('chat', messageHandler); - liveChat.on('error', errorHandler); - - req.on('close', () => { - liveChat.off('chat', messageHandler); - liveChat.off('error', errorHandler); + socket.emit('error', 'An error occurred with the live chat'); }); - } catch (error) { - next(error); - } - }; - - public stopLiveStreamChat = async (req: Request, res: Response, next: NextFunction): Promise => { - try { - const { liveId } = req.params; - const liveChat = this.activeLiveChats.get(liveId); - if (!liveChat) { - res.status(404).json({ error: 'Live chat not found' }); - return; + try { + await liveChat.start(); + console.log('Listening for live chat messages'); + } catch (error) { + console.error('Error starting LiveChat:', error); + socket.emit('error', 'Failed to start listening for live chat messages'); } + }; + + socket.on('startChat', startLiveChat); - await liveChat.stop(); - this.activeLiveChats.delete(liveId); - res.status(200).json({ message: 'Live chat stopped' }); - } catch (error) { - next(error); - } - }; + socket.on('disconnect', () => { + console.log('Client disconnected'); + if (liveChat) { + liveChat.stop(); + } + }); + }); +}; - private extractLiveId = (url: string): string | null => { - const regex = /(?:https?:\/\/)?(?:www\.)?(?:youtube\.com\/(?:watch\?v=|live\/)|youtu\.be\/)([a-zA-Z0-9_-]{11})/; - const match = url.match(regex); - return match ? match[1] : null; - }; -} +export { setupChatSocket }; \ No newline at end of file diff --git a/src/routes/chat.ts b/src/routes/chat.ts index b4cfb67..bdfb15a 100644 --- a/src/routes/chat.ts +++ b/src/routes/chat.ts @@ -1,11 +1,5 @@ import express from 'express'; -import { ChatController } from '../controllers/chatController'; const router = express.Router(); -const chatController = new ChatController(); -router.post('/livestream/start', chatController.startLiveStreamChat); -router.get('/livestream/:liveId/messages', chatController.getLiveStreamMessages); -router.post('/livestream/:liveId/stop', chatController.stopLiveStreamChat); - -export default router; +export default router; \ No newline at end of file