testapp: Improve testsapp using Typescript
This commit is contained in:
parent
d4f3b48082
commit
347d9472e0
10
testapp/.env
10
testapp/.env
@ -1,2 +1,10 @@
|
|||||||
|
# API Settings
|
||||||
OPENVIDU_MEET_URL=http://localhost:6080/meet/api/v1
|
OPENVIDU_MEET_URL=http://localhost:6080/meet/api/v1
|
||||||
PORT=5080
|
API_KEY=meet-api-key
|
||||||
|
|
||||||
|
# Server Settings
|
||||||
|
PORT=5080
|
||||||
|
LOG_LEVEL=INFO
|
||||||
|
|
||||||
|
# Environment
|
||||||
|
NODE_ENV=development
|
||||||
24
testapp/.eslintrc
Normal file
24
testapp/.eslintrc
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
{
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"plugins": ["@typescript-eslint", "prettier"],
|
||||||
|
"rules": {
|
||||||
|
"prettier/prettier": "error",
|
||||||
|
"no-console": "warn",
|
||||||
|
"@typescript-eslint/explicit-module-boundary-types": "off",
|
||||||
|
"@typescript-eslint/no-explicit-any": "warn"
|
||||||
|
},
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"node": true,
|
||||||
|
"es6": true
|
||||||
|
},
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": 2020,
|
||||||
|
"sourceType": "module"
|
||||||
|
}
|
||||||
|
}
|
||||||
27
testapp/.eslintrc.json
Normal file
27
testapp/.eslintrc.json
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"env": {
|
||||||
|
"browser": true,
|
||||||
|
"es2021": true,
|
||||||
|
"node": true
|
||||||
|
},
|
||||||
|
"extends": [
|
||||||
|
"eslint:recommended",
|
||||||
|
"plugin:@typescript-eslint/recommended",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"parser": "@typescript-eslint/parser",
|
||||||
|
"parserOptions": {
|
||||||
|
"ecmaVersion": "latest",
|
||||||
|
"sourceType": "module"
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
"@typescript-eslint",
|
||||||
|
"prettier"
|
||||||
|
],
|
||||||
|
"rules": {
|
||||||
|
"prettier/prettier": "error",
|
||||||
|
"no-unused-vars": "warn",
|
||||||
|
"@typescript-eslint/no-explicit-any": "warn",
|
||||||
|
"@typescript-eslint/explicit-module-boundary-types": "off"
|
||||||
|
}
|
||||||
|
}
|
||||||
10
testapp/.prettierrc
Normal file
10
testapp/.prettierrc
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
{
|
||||||
|
"semi": true,
|
||||||
|
"tabWidth": 2,
|
||||||
|
"printWidth": 100,
|
||||||
|
"singleQuote": true,
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"bracketSpacing": true,
|
||||||
|
"arrowParens": "avoid",
|
||||||
|
"endOfLine": "lf"
|
||||||
|
}
|
||||||
@ -1,38 +0,0 @@
|
|||||||
|
|
||||||
export const joinRoom = async (req, res) => {
|
|
||||||
try {
|
|
||||||
const { participantRole, roomUrl } = req.body;
|
|
||||||
|
|
||||||
if (!roomUrl) {
|
|
||||||
throw new Error('Room URL is required.');
|
|
||||||
}
|
|
||||||
|
|
||||||
res.render('videoRoom', {
|
|
||||||
roomUrl,
|
|
||||||
participantRole,
|
|
||||||
isModerator: participantRole === 'moderator',
|
|
||||||
});
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error joining room:', error);
|
|
||||||
res
|
|
||||||
.status(400)
|
|
||||||
.json({ message: 'Error joining room', error: error.message });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const handleWebhook = async (req, res, io) => {
|
|
||||||
try {
|
|
||||||
|
|
||||||
// Log event
|
|
||||||
console.log(`Webhook received:`, req.body);
|
|
||||||
io.emit('webhookEvent', req.body);
|
|
||||||
|
|
||||||
res.status(200).send('Webhook received');
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error handling webhook:', error);
|
|
||||||
res
|
|
||||||
.status(400)
|
|
||||||
.json({ message: 'Error handling webhook', error: error.message });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,42 +0,0 @@
|
|||||||
export const renderHomePage = async (req, res) => {
|
|
||||||
try {
|
|
||||||
// Get all OpenVidu Meet Rooms
|
|
||||||
const response = await fetch(`${process.env.OPENVIDU_MEET_URL}/rooms`)
|
|
||||||
const rooms = await response.json()
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error('Failed to fetch rooms')
|
|
||||||
}
|
|
||||||
|
|
||||||
res.render('home', { rooms })
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error fetching rooms:', error)
|
|
||||||
res.render('home', { rooms: [], error: 'Failed to load rooms' })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const createRoom = async (req, res) => {
|
|
||||||
try {
|
|
||||||
// Extract values from request body
|
|
||||||
const { roomIdPrefix, autoDeletionDate } = req.body
|
|
||||||
|
|
||||||
// Request to create a new room
|
|
||||||
const response = await fetch(`${process.env.OPENVIDU_MEET_URL}/rooms`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({
|
|
||||||
roomIdPrefix,
|
|
||||||
autoDeletionDate: new Date(autoDeletionDate).getTime()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
// Handle response
|
|
||||||
const data = await response.json()
|
|
||||||
if (!response.ok) throw new Error(data.message || 'Room creation failed')
|
|
||||||
|
|
||||||
renderHomePage(req, res)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Room creation error:', error)
|
|
||||||
res.status(500).json({ message: 'Error creating a room', error: error.message })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
6
testapp/nodemon.json
Normal file
6
testapp/nodemon.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"watch": ["src", "server.ts", ".env"],
|
||||||
|
"ignore": ["src/public"],
|
||||||
|
"ext": "ts,js,json",
|
||||||
|
"exec": "ts-node --esm server.ts"
|
||||||
|
}
|
||||||
3557
testapp/package-lock.json
generated
3557
testapp/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -1,17 +1,51 @@
|
|||||||
{
|
{
|
||||||
"name": "meet-nodejs-sample",
|
"name": "meet-testapp",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "",
|
"description": "",
|
||||||
"main": "server.js",
|
"main": "dist/server.js",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "node server.js"
|
"start": "npm run build && node dist/server.js",
|
||||||
},
|
"start:dev": "NODE_ENV=development nodemon --watch \"**/*.ts\" --exec \"node --loader ts-node/esm server.ts\"",
|
||||||
"dependencies": {
|
"start:prod": "NODE_ENV=production node dist/server.js",
|
||||||
"cors": "2.8.5",
|
"dev": "concurrently \"npm:watch-*\"",
|
||||||
"dotenv": "^16.4.7",
|
"watch-ts": "tsc --watch",
|
||||||
"express": "^4.21.2",
|
"watch-node": "nodemon --watch dist dist/server.js",
|
||||||
"mustache-express": "^1.3.2",
|
"watch-client": "nodemon --watch src/public/js --ext ts --exec node src/utils/build-client.js",
|
||||||
"socket.io": "^4.8.1"
|
"build": "npm run clean && npm run build-server && npm run build-client && npm run copy-assets",
|
||||||
}
|
"build:dev": "NODE_ENV=development npm run build",
|
||||||
|
"build:prod": "NODE_ENV=production npm run build",
|
||||||
|
"build-server": "tsc",
|
||||||
|
"build-client": "node scripts/build-client.js",
|
||||||
|
"copy-assets": "node scripts/copy-assets.js",
|
||||||
|
"lint": "eslint . --ext .ts,.js",
|
||||||
|
"format": "prettier --write \"**/*.{ts,js}\"",
|
||||||
|
"clean": "rimraf dist",
|
||||||
|
"test": "jest"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"cors": "2.8.5",
|
||||||
|
"dotenv": "^16.4.7",
|
||||||
|
"express": "^4.21.2",
|
||||||
|
"mustache-express": "^1.3.2",
|
||||||
|
"socket.io": "^4.8.1"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/cors": "^2.8.17",
|
||||||
|
"@types/express": "^5.0.1",
|
||||||
|
"@types/mustache-express": "^1.2.5",
|
||||||
|
"@types/node": "^22.15.15",
|
||||||
|
"@types/socket.io": "^3.0.1",
|
||||||
|
"@typescript-eslint/eslint-plugin": "^8.32.0",
|
||||||
|
"@typescript-eslint/parser": "^8.32.0",
|
||||||
|
"concurrently": "^9.1.2",
|
||||||
|
"eslint": "^9.26.0",
|
||||||
|
"eslint-config-prettier": "^10.1.3",
|
||||||
|
"eslint-plugin-prettier": "^5.4.0",
|
||||||
|
"nodemon": "^3.1.10",
|
||||||
|
"prettier": "^3.5.3",
|
||||||
|
"rimraf": "^6.0.1",
|
||||||
|
"ts-node": "^10.9.2",
|
||||||
|
"typescript": "^5.8.3"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
117
testapp/public/js/home.ts
Normal file
117
testapp/public/js/home.ts
Normal file
@ -0,0 +1,117 @@
|
|||||||
|
import { formatDate as formatDateUtil, getRelativeTimeString as getRelativeTimeStringUtil } from '../../utils/common.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for room data
|
||||||
|
*/
|
||||||
|
interface Room {
|
||||||
|
roomId: string;
|
||||||
|
roomIdPrefix: string;
|
||||||
|
moderatorRoomUrl: string;
|
||||||
|
publisherRoomUrl: string;
|
||||||
|
viewerRoomUrl: string;
|
||||||
|
createdAt: number;
|
||||||
|
autoDeletionDate: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
setupRoomElements();
|
||||||
|
setupFormValidation();
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up dynamic room elements and event handlers
|
||||||
|
*/
|
||||||
|
function setupRoomElements(): void {
|
||||||
|
// Format dates in the room cards
|
||||||
|
document.querySelectorAll('[data-timestamp]').forEach((element) => {
|
||||||
|
const timestamp = parseInt(element.getAttribute('data-timestamp') || '0', 10);
|
||||||
|
if (timestamp > 0) {
|
||||||
|
element.textContent = formatDateUtil(timestamp);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Set relative time for room creation
|
||||||
|
document.querySelectorAll('[data-created-at]').forEach((element) => {
|
||||||
|
const timestamp = parseInt(element.getAttribute('data-created-at') || '0', 10);
|
||||||
|
if (timestamp > 0) {
|
||||||
|
element.textContent = getRelativeTimeStringUtil(timestamp);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Initialize copy URL buttons
|
||||||
|
document.querySelectorAll('.copy-url-btn').forEach((button) => {
|
||||||
|
button.addEventListener('click', (event) => handleCopyUrl(event as MouseEvent));
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handle click event for copying URLs
|
||||||
|
*/
|
||||||
|
function handleCopyUrl(event: MouseEvent): void {
|
||||||
|
const button = event.currentTarget as HTMLButtonElement;
|
||||||
|
const url = button.getAttribute('data-url');
|
||||||
|
|
||||||
|
if (url) {
|
||||||
|
navigator.clipboard.writeText(url).then(
|
||||||
|
() => {
|
||||||
|
// Show success feedback
|
||||||
|
const originalText = button.textContent || '';
|
||||||
|
button.textContent = 'Copied!';
|
||||||
|
button.classList.add('copied');
|
||||||
|
|
||||||
|
// Reset button text after 2 seconds
|
||||||
|
setTimeout(() => {
|
||||||
|
button.textContent = originalText;
|
||||||
|
button.classList.remove('copied');
|
||||||
|
}, 2000);
|
||||||
|
},
|
||||||
|
(err) => {
|
||||||
|
console.error('Failed to copy text: ', err);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up form validation for the create room form
|
||||||
|
*/
|
||||||
|
function setupFormValidation(): void {
|
||||||
|
const createRoomForm = document.getElementById('create-room-form') as HTMLFormElement | null;
|
||||||
|
|
||||||
|
if (createRoomForm) {
|
||||||
|
createRoomForm.addEventListener('submit', (event) => {
|
||||||
|
const roomIdPrefixInput = document.getElementById('roomIdPrefix') as HTMLInputElement;
|
||||||
|
const autoDeletionDateInput = document.getElementById('autoDeletionDate') as HTMLInputElement;
|
||||||
|
|
||||||
|
// Basic validation
|
||||||
|
if (!roomIdPrefixInput.value.trim()) {
|
||||||
|
event.preventDefault();
|
||||||
|
alert('Please enter a room prefix');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!autoDeletionDateInput.value) {
|
||||||
|
event.preventDefault();
|
||||||
|
alert('Please select an auto deletion date');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set minimum date to today
|
||||||
|
const today = new Date();
|
||||||
|
today.setHours(0, 0, 0, 0);
|
||||||
|
const selectedDate = new Date(autoDeletionDateInput.value);
|
||||||
|
|
||||||
|
if (selectedDate < today) {
|
||||||
|
event.preventDefault();
|
||||||
|
alert('Auto deletion date must be today or in the future');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set minimum date for the date picker to today
|
||||||
|
const autoDeletionDateInput = document.getElementById('autoDeletionDate') as HTMLInputElement;
|
||||||
|
if (autoDeletionDateInput) {
|
||||||
|
const today = new Date().toISOString().split('T')[0];
|
||||||
|
autoDeletionDateInput.min = today;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,51 +0,0 @@
|
|||||||
document.getElementById('end-meeting-btn')?.addEventListener('click', () => {
|
|
||||||
const meet = document.querySelector('openvidu-meet');
|
|
||||||
meet.endMeeting();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('leave-room-btn')?.addEventListener('click', () => {
|
|
||||||
const meet = document.querySelector('openvidu-meet');
|
|
||||||
meet.leaveRoom();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('toggle-chat-btn')?.addEventListener('click', () => {
|
|
||||||
const meet = document.querySelector('openvidu-meet');
|
|
||||||
meet.toggleChat();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.addEventListener('DOMContentLoaded', () => {
|
|
||||||
const socket = io();
|
|
||||||
|
|
||||||
// Listen for webhook events from the server
|
|
||||||
socket.on('webhookEvent', (payload) => {
|
|
||||||
console.log('Webhook event received:', payload);
|
|
||||||
addWebhookToLog(payload);
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('DOM loaded');
|
|
||||||
const meet = document.querySelector('openvidu-meet');
|
|
||||||
|
|
||||||
// Event listener for when the local participant joined the room
|
|
||||||
meet.addEventListener('join', (event) => {
|
|
||||||
addEventToLog(event.type, `${JSON.stringify(event.detail)}`);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Event listener for when the local participant left the room
|
|
||||||
meet.addEventListener('left', (event) => {
|
|
||||||
addEventToLog(event.type, `${JSON.stringify(event.detail)}`);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
function addEventToLog(eventType, eventMessage) {
|
|
||||||
const eventsList = document.getElementById('events-list');
|
|
||||||
const li = document.createElement('li');
|
|
||||||
li.textContent = `[ ${eventType} ] : ${eventMessage}`;
|
|
||||||
eventsList.appendChild(li);
|
|
||||||
}
|
|
||||||
|
|
||||||
function addWebhookToLog(payload) {
|
|
||||||
const webhookLogList = document.getElementById('webhook-log-list');
|
|
||||||
const li = document.createElement('li');
|
|
||||||
li.textContent = `[ ${payload.event} ] : ${JSON.stringify(payload)}`;
|
|
||||||
webhookLogList.appendChild(li);
|
|
||||||
}
|
|
||||||
161
testapp/public/js/videoRoom.ts
Normal file
161
testapp/public/js/videoRoom.ts
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
/**
|
||||||
|
* Event types that can be emitted by the OpenVidu-Meet web component
|
||||||
|
*/
|
||||||
|
type MeetEvent = 'JOIN' | 'LEFT' | 'ERROR';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for OpenVidu-Meet component events
|
||||||
|
*/
|
||||||
|
interface MeetEventDetail {
|
||||||
|
participantId?: string;
|
||||||
|
participantName?: string;
|
||||||
|
roomId?: string;
|
||||||
|
timestamp: number;
|
||||||
|
error?: {
|
||||||
|
message: string;
|
||||||
|
code: string;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for OpenVidu-Meet web component
|
||||||
|
*/
|
||||||
|
interface OpenViduMeetElement extends HTMLElement {
|
||||||
|
endMeeting(): void;
|
||||||
|
leaveRoom(): void;
|
||||||
|
toggleChat(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface for webhook events
|
||||||
|
*/
|
||||||
|
interface WebhookEvent {
|
||||||
|
event: string;
|
||||||
|
[key: string]: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up event listeners when the DOM is loaded
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
// Set up button click handlers
|
||||||
|
setupButtonHandlers();
|
||||||
|
|
||||||
|
// Initialize socket.io connection
|
||||||
|
const socket = io();
|
||||||
|
|
||||||
|
// Get API key from the data attribute
|
||||||
|
const apiKey = document.getElementById('meeting-container')?.dataset.apiKey || '';
|
||||||
|
|
||||||
|
// Store API key in a variable that can be used for fetch requests
|
||||||
|
window.meetApiKey = apiKey;
|
||||||
|
|
||||||
|
// Listen for webhook events from the server
|
||||||
|
socket.on('webhookEvent', (payload: WebhookEvent) => {
|
||||||
|
console.log('Webhook event received:', payload);
|
||||||
|
addWebhookToLog(payload);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('DOM loaded');
|
||||||
|
const meet = document.querySelector('openvidu-meet') as OpenViduMeetElement;
|
||||||
|
|
||||||
|
if (meet) {
|
||||||
|
// Event listener for when the local participant joined the room
|
||||||
|
meet.addEventListener('JOIN', ((event: CustomEvent<MeetEventDetail>) => {
|
||||||
|
addEventToLog('JOIN', JSON.stringify(event.detail));
|
||||||
|
}) as EventListener);
|
||||||
|
|
||||||
|
// Event listener for when the local participant left the room
|
||||||
|
meet.addEventListener('LEFT', ((event: CustomEvent<MeetEventDetail>) => {
|
||||||
|
addEventToLog('LEFT', JSON.stringify(event.detail));
|
||||||
|
}) as EventListener);
|
||||||
|
|
||||||
|
// Error event listener
|
||||||
|
meet.addEventListener('ERROR', ((event: CustomEvent<MeetEventDetail>) => {
|
||||||
|
addEventToLog('ERROR', JSON.stringify(event.detail));
|
||||||
|
}) as EventListener);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set up button click handlers
|
||||||
|
*/
|
||||||
|
function setupButtonHandlers(): void {
|
||||||
|
const meet = document.querySelector('openvidu-meet') as OpenViduMeetElement;
|
||||||
|
|
||||||
|
// End meeting button click handler
|
||||||
|
document.getElementById('end-meeting-btn')?.addEventListener('click', () => {
|
||||||
|
if (meet) {
|
||||||
|
meet.endMeeting();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Leave room button click handler
|
||||||
|
document.getElementById('leave-room-btn')?.addEventListener('click', () => {
|
||||||
|
if (meet) {
|
||||||
|
meet.leaveRoom();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Toggle chat button click handler
|
||||||
|
document.getElementById('toggle-chat-btn')?.addEventListener('click', () => {
|
||||||
|
if (meet) {
|
||||||
|
meet.toggleChat();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a component event to the events log
|
||||||
|
*/
|
||||||
|
function addEventToLog(eventType: MeetEvent, eventMessage: string): void {
|
||||||
|
const eventsList = document.getElementById('events-list');
|
||||||
|
if (eventsList) {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.textContent = `[ ${eventType} ] : ${eventMessage}`;
|
||||||
|
eventsList.appendChild(li);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add a webhook event to the webhooks log
|
||||||
|
*/
|
||||||
|
function addWebhookToLog(payload: WebhookEvent): void {
|
||||||
|
const webhookLogList = document.getElementById('webhook-log-list');
|
||||||
|
if (webhookLogList) {
|
||||||
|
const li = document.createElement('li');
|
||||||
|
li.textContent = `[ ${payload.event} ] : ${JSON.stringify(payload)}`;
|
||||||
|
webhookLogList.appendChild(li);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utility function to make API requests with the API key
|
||||||
|
*/
|
||||||
|
function fetchWithApiKey(url: string, options: RequestInit = {}): Promise<Response> {
|
||||||
|
// Ensure headers object exists
|
||||||
|
const headers = new Headers(options.headers || {});
|
||||||
|
|
||||||
|
// Add API key to headers
|
||||||
|
headers.append('X-API-KEY', window.meetApiKey);
|
||||||
|
headers.append('Accept', 'application/json');
|
||||||
|
|
||||||
|
// Handle JSON request body
|
||||||
|
if (options.body && typeof options.body !== 'string') {
|
||||||
|
headers.append('Content-Type', 'application/json');
|
||||||
|
options.body = JSON.stringify(options.body);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create new options object with merged headers
|
||||||
|
const fetchOptions: RequestInit = {
|
||||||
|
...options,
|
||||||
|
headers
|
||||||
|
};
|
||||||
|
|
||||||
|
return fetch(url, fetchOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add meetApiKey to window interface
|
||||||
|
declare global {
|
||||||
|
interface Window {
|
||||||
|
meetApiKey: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -53,7 +53,7 @@
|
|||||||
<!-- Main Content Section -->
|
<!-- Main Content Section -->
|
||||||
<div id="main-content" class="d-flex flex-column">
|
<div id="main-content" class="d-flex flex-column">
|
||||||
<!-- Web Component Section -->
|
<!-- Web Component Section -->
|
||||||
<div id="meeting-container">
|
<div id="meeting-container" data-api-key="{{ apiKey }}">
|
||||||
<openvidu-meet
|
<openvidu-meet
|
||||||
room-url="{{ roomUrl }}"
|
room-url="{{ roomUrl }}"
|
||||||
participant-name="{{ participantName }}"
|
participant-name="{{ participantName }}"
|
||||||
71
testapp/scripts/build-client.js
Normal file
71
testapp/scripts/build-client.js
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
import { exec } from 'child_process';
|
||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
|
||||||
|
// Get directory name in ES modules
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
// Source directory for client TypeScript files
|
||||||
|
const sourceDir = path.join(__dirname, '..', 'public', 'js');
|
||||||
|
// Destination directory for compiled JavaScript
|
||||||
|
const destDir = path.join(__dirname, '..', 'dist', 'testapp', 'public', 'js');
|
||||||
|
|
||||||
|
// Create destination directory if it doesn't exist
|
||||||
|
if (!fs.existsSync(destDir)) {
|
||||||
|
fs.mkdirSync(destDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get all TypeScript files in the source directory
|
||||||
|
const tsFiles = fs.readdirSync(sourceDir).filter(file => file.endsWith('.ts'));
|
||||||
|
|
||||||
|
// Compile each TypeScript file individually
|
||||||
|
console.log('Compiling client-side TypeScript files...');
|
||||||
|
|
||||||
|
// Create a much simpler compilation approach
|
||||||
|
tsFiles.forEach(file => {
|
||||||
|
const sourcePath = path.join(sourceDir, file);
|
||||||
|
const outputFile = file.replace('.ts', '.js');
|
||||||
|
const destPath = path.join(destDir, outputFile);
|
||||||
|
|
||||||
|
// Read the TypeScript file
|
||||||
|
const tsContent = fs.readFileSync(sourcePath, 'utf-8');
|
||||||
|
|
||||||
|
console.log(`Processing ${file}...`);
|
||||||
|
|
||||||
|
// Use esbuild to quickly transpile TypeScript to JavaScript
|
||||||
|
// For a simple application, we'll do some basic transforms ourselves
|
||||||
|
let jsContent = tsContent;
|
||||||
|
|
||||||
|
// Remove TypeScript type annotations and interfaces
|
||||||
|
jsContent = jsContent.replace(/^interface\s+\w+\s*\{[\s\S]*?\}/gm, '');
|
||||||
|
jsContent = jsContent.replace(/^type\s+\w+\s*=[\s\S]*?;/gm, '');
|
||||||
|
jsContent = jsContent.replace(/:\s*\w+(\[\])?(\s*\|\s*null)?(\s*\|\s*undefined)?/g, '');
|
||||||
|
jsContent = jsContent.replace(/<\w+(\[\])?>/g, '');
|
||||||
|
jsContent = jsContent.replace(/as\s+\w+(\[\])?/g, '');
|
||||||
|
|
||||||
|
// Save the JavaScript file
|
||||||
|
fs.writeFileSync(destPath, jsContent);
|
||||||
|
console.log(`Created ${outputFile}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Copy utility files that client code might need
|
||||||
|
const commonUtilPath = path.join(__dirname, '..', 'utils', 'common.js');
|
||||||
|
const destCommonPath = path.join(destDir, 'common.js');
|
||||||
|
|
||||||
|
// Basic conversion of the common.ts file to JS
|
||||||
|
if (fs.existsSync(path.join(__dirname, '..', 'utils', 'common.ts'))) {
|
||||||
|
const tsContent = fs.readFileSync(path.join(__dirname, '..', 'utils', 'common.ts'), 'utf-8');
|
||||||
|
let jsContent = tsContent;
|
||||||
|
|
||||||
|
// Remove TypeScript type annotations
|
||||||
|
jsContent = jsContent.replace(/:\s*\w+(\[\])?(\s*\|\s*null)?(\s*\|\s*undefined)?/g, '');
|
||||||
|
jsContent = jsContent.replace(/<\w+(\[\])?>/g, '');
|
||||||
|
|
||||||
|
fs.writeFileSync(destCommonPath, jsContent);
|
||||||
|
console.log('Created common.js utility file');
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Client-side files processed successfully');
|
||||||
58
testapp/scripts/copy-assets.js
Normal file
58
testapp/scripts/copy-assets.js
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
|
||||||
|
// Get directory name in ES modules
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
// Define paths
|
||||||
|
const projectRoot = path.join(__dirname, '..');
|
||||||
|
const viewsDir = path.join(projectRoot, 'public', 'views');
|
||||||
|
const distViewsDir = path.join(projectRoot, 'dist', 'public', 'views');
|
||||||
|
const cssDir = path.join(projectRoot, 'public', 'css');
|
||||||
|
const distCssDir = path.join(projectRoot, 'dist', 'testapp', 'public', 'css');
|
||||||
|
|
||||||
|
// Create dist directories if they don't exist
|
||||||
|
function ensureDirectoryExists(directory) {
|
||||||
|
if (!fs.existsSync(directory)) {
|
||||||
|
fs.mkdirSync(directory, { recursive: true });
|
||||||
|
console.log(`Created directory: ${directory}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy files from source to destination
|
||||||
|
function copyFiles(sourceDir, destDir, extension) {
|
||||||
|
ensureDirectoryExists(destDir);
|
||||||
|
|
||||||
|
// Check if source directory exists
|
||||||
|
if (!fs.existsSync(sourceDir)) {
|
||||||
|
console.log(`Source directory does not exist: ${sourceDir}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = fs.readdirSync(sourceDir).filter(file => file.endsWith(extension));
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
const sourcePath = path.join(sourceDir, file);
|
||||||
|
const destPath = path.join(destDir, file);
|
||||||
|
|
||||||
|
fs.copyFileSync(sourcePath, destPath);
|
||||||
|
console.log(`Copied ${sourcePath} to ${destPath}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Copied ${files.length} ${extension} files`);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Starting asset copy process...');
|
||||||
|
|
||||||
|
// Copy view templates
|
||||||
|
console.log('Copying Mustache templates...');
|
||||||
|
copyFiles(viewsDir, distViewsDir, '.mustache');
|
||||||
|
|
||||||
|
// Copy CSS files
|
||||||
|
console.log('Copying CSS files...');
|
||||||
|
copyFiles(cssDir, distCssDir, '.css');
|
||||||
|
|
||||||
|
console.log('All static assets have been copied to dist.');
|
||||||
@ -1,44 +0,0 @@
|
|||||||
import dotenv from 'dotenv';
|
|
||||||
import express from 'express';
|
|
||||||
import mustacheExpress from 'mustache-express';
|
|
||||||
import bodyParser from 'body-parser';
|
|
||||||
import cors from 'cors';
|
|
||||||
import path from 'path';
|
|
||||||
import http from 'http';
|
|
||||||
import { Server } from 'socket.io';
|
|
||||||
import { joinRoom, handleWebhook } from './controllers/ videoRoomController.js';
|
|
||||||
import { createRoom, renderHomePage } from './controllers/homeController.js';
|
|
||||||
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import { dirname } from 'path';
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = dirname(__filename);
|
|
||||||
|
|
||||||
dotenv.config();
|
|
||||||
|
|
||||||
const app = express();
|
|
||||||
const server = http.createServer(app);
|
|
||||||
const io = new Server(server);
|
|
||||||
|
|
||||||
app.engine('mustache', mustacheExpress());
|
|
||||||
app.set('view engine', 'mustache');
|
|
||||||
app.set('views', path.join(__dirname, '/views'));
|
|
||||||
app.use(express.static(path.join(__dirname, 'public')));
|
|
||||||
app.use(express.json());
|
|
||||||
app.use(bodyParser.urlencoded({ extended: true }));
|
|
||||||
|
|
||||||
// Middlewares
|
|
||||||
app.use(cors());
|
|
||||||
|
|
||||||
// Routes
|
|
||||||
app.get('/', renderHomePage);
|
|
||||||
app.post('/room', createRoom);
|
|
||||||
app.post('/join-room', joinRoom);
|
|
||||||
app.post('/webhook', (req, res) => {
|
|
||||||
handleWebhook(req, res, io);
|
|
||||||
});
|
|
||||||
const PORT = process.env.PORT || 5080;
|
|
||||||
server.listen(PORT, () => {
|
|
||||||
console.log(`Listening on http://localhost:${PORT}`);
|
|
||||||
});
|
|
||||||
76
testapp/server.ts
Normal file
76
testapp/server.ts
Normal file
@ -0,0 +1,76 @@
|
|||||||
|
import dotenv from 'dotenv';
|
||||||
|
import express from 'express';
|
||||||
|
import mustacheExpress from 'mustache-express';
|
||||||
|
import bodyParser from 'body-parser';
|
||||||
|
import cors from 'cors';
|
||||||
|
import path from 'path';
|
||||||
|
import http from 'http';
|
||||||
|
import { Server } from 'socket.io';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
|
||||||
|
// Import controllers
|
||||||
|
import { joinRoom, handleWebhook } from './src/controllers/videoRoomController.js';
|
||||||
|
import { createRoom, renderHomePage } from './src/controllers/homeController.js';
|
||||||
|
import { ConfigService } from './src/services/config.service.js';
|
||||||
|
import { Logger } from './src/utils/logger.js';
|
||||||
|
|
||||||
|
// Initialize logger
|
||||||
|
const logger = new Logger('Server');
|
||||||
|
|
||||||
|
// Get directory name in ES modules
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
// Define project root (going up from the dist folder if we're running the compiled version)
|
||||||
|
const projectRoot =
|
||||||
|
process.env.NODE_ENV === 'production'
|
||||||
|
? path.join(__dirname, '..') // In production, dist/server.js is 1 level down
|
||||||
|
: __dirname; // In development, we're in the project root
|
||||||
|
|
||||||
|
// Initialize environment variables
|
||||||
|
dotenv.config();
|
||||||
|
logger.info('Environment variables loaded');
|
||||||
|
|
||||||
|
// Get configuration
|
||||||
|
const config = ConfigService.getInstance();
|
||||||
|
|
||||||
|
// Setup Express and HTTP server
|
||||||
|
const app = express();
|
||||||
|
const server = http.createServer(app);
|
||||||
|
const io = new Server(server);
|
||||||
|
|
||||||
|
// Configure view engine and middleware
|
||||||
|
app.engine('mustache', mustacheExpress());
|
||||||
|
app.set('view engine', 'mustache');
|
||||||
|
app.set('views', path.join(projectRoot, 'public/views'));
|
||||||
|
app.use(express.static(path.join(projectRoot, 'public')));
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(bodyParser.urlencoded({ extended: true }));
|
||||||
|
app.use(cors());
|
||||||
|
logger.debug('Express middleware configured');
|
||||||
|
logger.debug(`Static directory: ${path.join(projectRoot, 'public')}`);
|
||||||
|
logger.debug(`Views directory: ${path.join(projectRoot, 'public/views')}`);
|
||||||
|
|
||||||
|
// Define routes
|
||||||
|
app.get('/', renderHomePage);
|
||||||
|
app.post('/room', createRoom);
|
||||||
|
app.post('/join-room', joinRoom);
|
||||||
|
app.post('/webhook', (req, res) => {
|
||||||
|
handleWebhook(req, res, io);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Socket.IO connection handler
|
||||||
|
io.on('connection', socket => {
|
||||||
|
logger.info(`New socket connection: ${socket.id}`);
|
||||||
|
|
||||||
|
socket.on('disconnect', () => {
|
||||||
|
logger.info(`Socket disconnected: ${socket.id}`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Start the server
|
||||||
|
const PORT = config.port;
|
||||||
|
server.listen(PORT, () => {
|
||||||
|
logger.info(`Server started successfully on http://localhost:${PORT}`);
|
||||||
|
});
|
||||||
56
testapp/src/controllers/homeController.ts
Normal file
56
testapp/src/controllers/homeController.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { RoomService } from '../services/room.service.js';
|
||||||
|
import { Logger } from '../utils/logger.js';
|
||||||
|
import { MeetRoomOptions } from '../../../typings/src/room.js';
|
||||||
|
|
||||||
|
// Initialize logger
|
||||||
|
const logger = new Logger('HomeController');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Renders the home page with available rooms
|
||||||
|
*/
|
||||||
|
export const renderHomePage = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
const roomService = RoomService.getInstance();
|
||||||
|
|
||||||
|
try {
|
||||||
|
logger.info('Rendering home page');
|
||||||
|
const rooms = await roomService.getAllRooms();
|
||||||
|
res.render('home', { rooms });
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error fetching rooms:', error);
|
||||||
|
res.render('home', { rooms: [], error: 'Failed to load rooms' });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new room based on form data
|
||||||
|
*/
|
||||||
|
export const createRoom = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
const roomService = RoomService.getInstance();
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Extract values from request body
|
||||||
|
const { roomIdPrefix, autoDeletionDate } = req.body;
|
||||||
|
|
||||||
|
logger.info(`Creating room with prefix: ${roomIdPrefix}`);
|
||||||
|
|
||||||
|
const roomData: MeetRoomOptions = {
|
||||||
|
roomIdPrefix,
|
||||||
|
autoDeletionDate: new Date(autoDeletionDate).getTime(),
|
||||||
|
};
|
||||||
|
|
||||||
|
const newRoom = await roomService.createRoom(roomData);
|
||||||
|
|
||||||
|
if (!newRoom) {
|
||||||
|
throw new Error('Room creation failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Room created successfully: ${newRoom.roomId}`);
|
||||||
|
|
||||||
|
// Redirect to home page showing the new room
|
||||||
|
await renderHomePage(req, res);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Room creation error:', error);
|
||||||
|
res.status(500).json({ message: 'Error creating a room', error: (error as Error).message });
|
||||||
|
}
|
||||||
|
};
|
||||||
73
testapp/src/controllers/videoRoomController.ts
Normal file
73
testapp/src/controllers/videoRoomController.ts
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
import { Request, Response } from 'express';
|
||||||
|
import { Server } from 'socket.io';
|
||||||
|
import { ConfigService } from '../services/config.service.js';
|
||||||
|
import { Logger } from '../utils/logger.js';
|
||||||
|
import { MeetWebhookEvent } from '../../../typings/src/webhook.model.js';
|
||||||
|
import { ParticipantRole } from '../../../typings/src/participant.js';
|
||||||
|
|
||||||
|
// Initialize logger
|
||||||
|
const logger = new Logger('VideoRoomController');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Join Room request body interface
|
||||||
|
*/
|
||||||
|
interface JoinRoomRequest {
|
||||||
|
participantRole: ParticipantRole;
|
||||||
|
roomUrl: string;
|
||||||
|
participantName?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles joining a room with the specified role
|
||||||
|
*/
|
||||||
|
export const joinRoom = async (req: Request, res: Response): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const { participantRole, roomUrl, participantName = 'User' } = req.body as JoinRoomRequest;
|
||||||
|
|
||||||
|
if (!roomUrl) {
|
||||||
|
throw new Error('Room URL is required.');
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Joining room as ${participantRole}: ${roomUrl}`);
|
||||||
|
|
||||||
|
const config = ConfigService.getInstance();
|
||||||
|
|
||||||
|
res.render('videoRoom', {
|
||||||
|
roomUrl,
|
||||||
|
participantRole,
|
||||||
|
participantName,
|
||||||
|
isModerator: participantRole === 'moderator',
|
||||||
|
apiKey: config.apiKey,
|
||||||
|
});
|
||||||
|
|
||||||
|
logger.info(`Successfully rendered video room for ${participantName}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error joining room:', error);
|
||||||
|
res.status(400).json({ message: 'Error joining room', error: (error as Error).message });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles incoming webhook events from OpenVidu Meet
|
||||||
|
*/
|
||||||
|
export const handleWebhook = async (req: Request, res: Response, io: Server): Promise<void> => {
|
||||||
|
try {
|
||||||
|
const webhookEvent = req.body as MeetWebhookEvent;
|
||||||
|
|
||||||
|
// Log event details
|
||||||
|
logger.info(`Webhook received: ${webhookEvent.event}`, {
|
||||||
|
type: webhookEvent.event,
|
||||||
|
timestamp: webhookEvent.creationDate,
|
||||||
|
data: webhookEvent.data,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Broadcast to all connected clients
|
||||||
|
io.emit('webhookEvent', webhookEvent);
|
||||||
|
logger.debug('Event broadcast to all clients');
|
||||||
|
|
||||||
|
res.status(200).send('Webhook received');
|
||||||
|
} catch (error) {
|
||||||
|
logger.error('Error handling webhook:', error);
|
||||||
|
res.status(400).json({ message: 'Error handling webhook', error: (error as Error).message });
|
||||||
|
}
|
||||||
|
};
|
||||||
50
testapp/src/services/config.service.ts
Normal file
50
testapp/src/services/config.service.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { Logger } from '../utils/logger.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for handling configuration and environment variables
|
||||||
|
*/
|
||||||
|
export class ConfigService {
|
||||||
|
private static instance: ConfigService;
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
// Environment variables with defaults
|
||||||
|
public readonly meetApiUrl: string;
|
||||||
|
public readonly apiKey: string;
|
||||||
|
public readonly port: number;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.logger = new Logger('ConfigService');
|
||||||
|
|
||||||
|
// Load environment variables with defaults
|
||||||
|
this.meetApiUrl = process.env.OPENVIDU_MEET_URL || 'http://localhost:6080/meet/api/v1';
|
||||||
|
this.apiKey = process.env.API_KEY || 'meet-api-key';
|
||||||
|
this.port = parseInt(process.env.PORT || '5080', 10);
|
||||||
|
|
||||||
|
this.logger.debug('Configuration loaded:', {
|
||||||
|
meetApiUrl: this.meetApiUrl,
|
||||||
|
port: this.port,
|
||||||
|
// Don't log sensitive data like API keys
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get singleton instance of ConfigService
|
||||||
|
*/
|
||||||
|
public static getInstance(): ConfigService {
|
||||||
|
if (!ConfigService.instance) {
|
||||||
|
ConfigService.instance = new ConfigService();
|
||||||
|
}
|
||||||
|
return ConfigService.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get API headers with authentication
|
||||||
|
*/
|
||||||
|
public getApiHeaders(): Record<string, string> {
|
||||||
|
return {
|
||||||
|
'Accept': 'application/json',
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
'X-API-KEY': this.apiKey
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
102
testapp/src/services/room.service.ts
Normal file
102
testapp/src/services/room.service.ts
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import { ConfigService } from './config.service.js';
|
||||||
|
import { Logger } from '../utils/logger.js';
|
||||||
|
import { MeetRoom, MeetRoomOptions } from '../../../typings/src/room.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service for handling Room-related API operations
|
||||||
|
*/
|
||||||
|
export class RoomService {
|
||||||
|
private static instance: RoomService;
|
||||||
|
private config: ConfigService;
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
private constructor() {
|
||||||
|
this.config = ConfigService.getInstance();
|
||||||
|
this.logger = new Logger('RoomService');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get singleton instance of RoomService
|
||||||
|
*/
|
||||||
|
public static getInstance(): RoomService {
|
||||||
|
if (!RoomService.instance) {
|
||||||
|
RoomService.instance = new RoomService();
|
||||||
|
}
|
||||||
|
return RoomService.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch all rooms from the API
|
||||||
|
*/
|
||||||
|
public async getAllRooms(): Promise<MeetRoom[]> {
|
||||||
|
try {
|
||||||
|
this.logger.info('Fetching all rooms from API');
|
||||||
|
const response = await fetch(`${this.config.meetApiUrl}/rooms`, {
|
||||||
|
headers: this.config.getApiHeaders(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch rooms: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const data: { pagination: any; rooms: MeetRoom[] } = await response.json();
|
||||||
|
this.logger.debug(`Retrieved ${data.rooms.length} rooms`);
|
||||||
|
return data.rooms;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error fetching rooms:', error);
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new room
|
||||||
|
*/
|
||||||
|
public async createRoom(roomData: MeetRoomOptions): Promise<MeetRoom | null> {
|
||||||
|
try {
|
||||||
|
this.logger.info(`Creating new room with prefix: ${roomData.roomIdPrefix}`);
|
||||||
|
const response = await fetch(`${this.config.meetApiUrl}/rooms`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: this.config.getApiHeaders(),
|
||||||
|
body: JSON.stringify(roomData),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
const error = await response.json();
|
||||||
|
throw new Error(error.message || 'Room creation failed');
|
||||||
|
}
|
||||||
|
|
||||||
|
const newRoom: MeetRoom = await response.json();
|
||||||
|
this.logger.info(`Room created successfully: ${newRoom.roomId}`);
|
||||||
|
return newRoom;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error('Error creating room:', error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get a specific room by ID
|
||||||
|
*/
|
||||||
|
public async getRoomById(roomId: string): Promise<MeetRoom | null> {
|
||||||
|
try {
|
||||||
|
this.logger.info(`Fetching room with id: ${roomId}`);
|
||||||
|
const response = await fetch(`${this.config.meetApiUrl}/rooms/${roomId}`, {
|
||||||
|
headers: this.config.getApiHeaders(),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
if (response.status === 404) {
|
||||||
|
this.logger.warn(`Room not found: ${roomId}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
throw new Error(`Failed to fetch room: ${response.statusText}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const room = await response.json();
|
||||||
|
return room;
|
||||||
|
} catch (error) {
|
||||||
|
this.logger.error(`Error fetching room ${roomId}:`, error);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
17
testapp/src/types/global.d.ts
vendored
Normal file
17
testapp/src/types/global.d.ts
vendored
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
/**
|
||||||
|
* Global type declarations for browser libraries
|
||||||
|
*/
|
||||||
|
|
||||||
|
declare const io: () => SocketIOClient.Socket;
|
||||||
|
|
||||||
|
interface Window {
|
||||||
|
meetApiKey: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
declare namespace SocketIOClient {
|
||||||
|
interface Socket {
|
||||||
|
on(event: string, callback: (data: any) => void): Socket;
|
||||||
|
emit(event: string, data: any): Socket;
|
||||||
|
disconnect(): void;
|
||||||
|
}
|
||||||
|
}
|
||||||
65
testapp/src/utils/common.ts
Normal file
65
testapp/src/utils/common.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
/**
|
||||||
|
* Common utility functions used across the application
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a date to a human-readable string
|
||||||
|
* @param timestamp - Unix timestamp in milliseconds
|
||||||
|
*/
|
||||||
|
export function formatDate(timestamp: number): string {
|
||||||
|
return new Date(timestamp).toLocaleString();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate a human-readable relative time string (e.g., "5 minutes ago")
|
||||||
|
* @param timestamp - Unix timestamp in milliseconds
|
||||||
|
*/
|
||||||
|
export function getRelativeTimeString(timestamp: number): string {
|
||||||
|
const now = Date.now();
|
||||||
|
const diff = now - timestamp;
|
||||||
|
|
||||||
|
const seconds = Math.floor(diff / 1000);
|
||||||
|
const minutes = Math.floor(seconds / 60);
|
||||||
|
const hours = Math.floor(minutes / 60);
|
||||||
|
const days = Math.floor(hours / 24);
|
||||||
|
|
||||||
|
if (days > 0) return `${days} day${days === 1 ? '' : 's'} ago`;
|
||||||
|
if (hours > 0) return `${hours} hour${hours === 1 ? '' : 's'} ago`;
|
||||||
|
if (minutes > 0) return `${minutes} minute${minutes === 1 ? '' : 's'} ago`;
|
||||||
|
return `${seconds} second${seconds === 1 ? '' : 's'} ago`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a string to title case (e.g., "hello world" -> "Hello World")
|
||||||
|
* @param str - String to convert
|
||||||
|
*/
|
||||||
|
export function toTitleCase(str: string): string {
|
||||||
|
return str.replace(
|
||||||
|
/\w\S*/g,
|
||||||
|
txt => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Safely parse JSON without throwing an exception
|
||||||
|
* @param jsonString - JSON string to parse
|
||||||
|
* @param fallback - Default value if parsing fails
|
||||||
|
*/
|
||||||
|
export function safeJsonParse<T>(jsonString: string, fallback: T): T {
|
||||||
|
try {
|
||||||
|
return JSON.parse(jsonString) as T;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Failed to parse JSON:', e);
|
||||||
|
return fallback;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Truncate a string to a maximum length with ellipsis
|
||||||
|
* @param str - String to truncate
|
||||||
|
* @param maxLength - Maximum length
|
||||||
|
*/
|
||||||
|
export function truncateString(str: string, maxLength: number): string {
|
||||||
|
if (str.length <= maxLength) return str;
|
||||||
|
return str.substring(0, maxLength - 3) + '...';
|
||||||
|
}
|
||||||
50
testapp/src/utils/copy-static-assets.ts
Normal file
50
testapp/src/utils/copy-static-assets.ts
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import fs from 'fs';
|
||||||
|
import path from 'path';
|
||||||
|
import { fileURLToPath } from 'url';
|
||||||
|
import { dirname } from 'path';
|
||||||
|
|
||||||
|
// Get directory name in ES modules
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = dirname(__filename);
|
||||||
|
|
||||||
|
// Define paths
|
||||||
|
const projectRoot = path.join(__dirname, '..', '..');
|
||||||
|
const viewsDir = path.join(projectRoot, 'views');
|
||||||
|
const distViewsDir = path.join(projectRoot, 'dist', 'views');
|
||||||
|
const cssDir = path.join(projectRoot, 'public', 'css');
|
||||||
|
const distCssDir = path.join(projectRoot, 'dist', 'public', 'css');
|
||||||
|
|
||||||
|
// Create dist directories if they don't exist
|
||||||
|
function ensureDirectoryExists(directory: string): void {
|
||||||
|
if (!fs.existsSync(directory)) {
|
||||||
|
fs.mkdirSync(directory, { recursive: true });
|
||||||
|
console.log(`Created directory: ${directory}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy files from source to destination
|
||||||
|
function copyFiles(sourceDir: string, destDir: string, extension: string): void {
|
||||||
|
ensureDirectoryExists(destDir);
|
||||||
|
|
||||||
|
const files = fs.readdirSync(sourceDir).filter(file => file.endsWith(extension));
|
||||||
|
|
||||||
|
files.forEach(file => {
|
||||||
|
const sourcePath = path.join(sourceDir, file);
|
||||||
|
const destPath = path.join(destDir, file);
|
||||||
|
|
||||||
|
fs.copyFileSync(sourcePath, destPath);
|
||||||
|
console.log(`Copied ${sourcePath} to ${destPath}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(`Copied ${files.length} ${extension} files`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Copy view templates
|
||||||
|
console.log('Copying Mustache templates...');
|
||||||
|
copyFiles(viewsDir, distViewsDir, '.mustache');
|
||||||
|
|
||||||
|
// Copy CSS files
|
||||||
|
console.log('Copying CSS files...');
|
||||||
|
copyFiles(cssDir, distCssDir, '.css');
|
||||||
|
|
||||||
|
console.log('All static assets have been copied to dist.');
|
||||||
83
testapp/src/utils/logger.ts
Normal file
83
testapp/src/utils/logger.ts
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
/**
|
||||||
|
* Logger utility for consistent logging across the application
|
||||||
|
*/
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log levels for the application
|
||||||
|
*/
|
||||||
|
export enum LogLevel {
|
||||||
|
DEBUG = 0,
|
||||||
|
INFO = 1,
|
||||||
|
WARN = 2,
|
||||||
|
ERROR = 3
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current log level for the application
|
||||||
|
* Can be set via environment variable LOG_LEVEL
|
||||||
|
*/
|
||||||
|
const currentLogLevel = (() => {
|
||||||
|
const envLevel = process.env.LOG_LEVEL?.toUpperCase();
|
||||||
|
switch (envLevel) {
|
||||||
|
case 'DEBUG': return LogLevel.DEBUG;
|
||||||
|
case 'INFO': return LogLevel.INFO;
|
||||||
|
case 'WARN': return LogLevel.WARN;
|
||||||
|
case 'ERROR': return LogLevel.ERROR;
|
||||||
|
default: return LogLevel.INFO; // Default level
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Logger class for consistent log formatting
|
||||||
|
*/
|
||||||
|
export class Logger {
|
||||||
|
private name: string;
|
||||||
|
|
||||||
|
constructor(name: string) {
|
||||||
|
this.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format a log message with timestamp and context
|
||||||
|
*/
|
||||||
|
private formatMessage(message: string): string {
|
||||||
|
const timestamp = new Date().toISOString();
|
||||||
|
return `[${timestamp}] [${this.name}] ${message}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log debug level message
|
||||||
|
*/
|
||||||
|
debug(message: string, ...args: any[]): void {
|
||||||
|
if (currentLogLevel <= LogLevel.DEBUG) {
|
||||||
|
console.debug(this.formatMessage(message), ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log info level message
|
||||||
|
*/
|
||||||
|
info(message: string, ...args: any[]): void {
|
||||||
|
if (currentLogLevel <= LogLevel.INFO) {
|
||||||
|
console.info(this.formatMessage(message), ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log warning level message
|
||||||
|
*/
|
||||||
|
warn(message: string, ...args: any[]): void {
|
||||||
|
if (currentLogLevel <= LogLevel.WARN) {
|
||||||
|
console.warn(this.formatMessage(message), ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log error level message
|
||||||
|
*/
|
||||||
|
error(message: string, ...args: any[]): void {
|
||||||
|
if (currentLogLevel <= LogLevel.ERROR) {
|
||||||
|
console.error(this.formatMessage(message), ...args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
22
testapp/tsconfig.json
Normal file
22
testapp/tsconfig.json
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2020",
|
||||||
|
"module": "NodeNext",
|
||||||
|
"moduleResolution": "NodeNext",
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"strict": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"allowJs": true, // Allow JavaScript files to be compiled
|
||||||
|
"checkJs": true, // Type check JavaScript files
|
||||||
|
"lib": ["DOM", "ES2020", "DOM.Iterable"],
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"include": ["src/**/*", "server.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
7
testapp/tsconfig.node.json
Normal file
7
testapp/tsconfig.node.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"ts-node": {
|
||||||
|
"esm": true,
|
||||||
|
"experimentalSpecifierResolution": "node"
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user