testapp: add kick participant functionality and fix view recordings link
- Enhanced the web component to include a kick participant feature. - Updated the UI to allow moderators to input participant identity for kicking. - Modified the room controller to handle the new kick participant request. - Refactored code for better readability and maintainability across various services and controllers. - Updated the index.mustache and room.mustache templates to reflect changes in functionality.
This commit is contained in:
parent
0a028226f3
commit
2b7466c6e3
@ -1,9 +1,10 @@
|
||||
const socket = (window as any).io();
|
||||
|
||||
let meet: {
|
||||
endMeeting: () => void;
|
||||
leaveRoom: () => void;
|
||||
on: (event: string, callback: (event: CustomEvent<any>) => void) => void;
|
||||
endMeeting: () => void;
|
||||
leaveRoom: () => void;
|
||||
kickParticipant: (participantIdentity: string) => void;
|
||||
on: (event: string, callback: (event: CustomEvent<any>) => void) => void;
|
||||
};
|
||||
let roomId: string | undefined;
|
||||
let showAllWebhooksCheckbox: HTMLInputElement | null;
|
||||
@ -12,238 +13,243 @@ let showAllWebhooksCheckbox: HTMLInputElement | null;
|
||||
* Add a component event to the events log
|
||||
*/
|
||||
const addEventToLog = (eventType: string, eventMessage: string): void => {
|
||||
const eventsList = document.getElementById('events-list');
|
||||
if (eventsList) {
|
||||
const li = document.createElement('li');
|
||||
li.className = `event-${eventType}`;
|
||||
li.textContent = `[ ${eventType} ] : ${eventMessage}`;
|
||||
eventsList.insertBefore(li, eventsList.firstChild);
|
||||
}
|
||||
const eventsList = document.getElementById('events-list');
|
||||
if (eventsList) {
|
||||
const li = document.createElement('li');
|
||||
li.className = `event-${eventType}`;
|
||||
li.textContent = `[ ${eventType} ] : ${eventMessage}`;
|
||||
eventsList.insertBefore(li, eventsList.firstChild);
|
||||
}
|
||||
};
|
||||
const escapeHtml = (unsafe: string): string => {
|
||||
return unsafe
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
return unsafe
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
};
|
||||
|
||||
const getWebhookEventsFromStorage = (roomId: string): any[] => {
|
||||
const data = localStorage.getItem('webhookEventsByRoom');
|
||||
if (!data) return [];
|
||||
const map = JSON.parse(data);
|
||||
return map[roomId] || [];
|
||||
const data = localStorage.getItem('webhookEventsByRoom');
|
||||
if (!data) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const map = JSON.parse(data);
|
||||
return map[roomId] || [];
|
||||
};
|
||||
|
||||
const saveWebhookEventToStorage = (roomId: string, event: any): void => {
|
||||
const data = localStorage.getItem('webhookEventsByRoom');
|
||||
const map = data ? JSON.parse(data) : {};
|
||||
if (!map[roomId]) map[roomId] = [];
|
||||
map[roomId].push(event);
|
||||
localStorage.setItem('webhookEventsByRoom', JSON.stringify(map));
|
||||
const data = localStorage.getItem('webhookEventsByRoom');
|
||||
const map = data ? JSON.parse(data) : {};
|
||||
if (!map[roomId]) {
|
||||
map[roomId] = [];
|
||||
}
|
||||
|
||||
map[roomId].push(event);
|
||||
localStorage.setItem('webhookEventsByRoom', JSON.stringify(map));
|
||||
};
|
||||
|
||||
const clearWebhookEventsByRoom = (roomId: string): void => {
|
||||
const data = localStorage.getItem('webhookEventsByRoom');
|
||||
if (!data) return;
|
||||
const map = JSON.parse(data);
|
||||
if (map[roomId]) {
|
||||
map[roomId] = [];
|
||||
localStorage.setItem('webhookEventsByRoom', JSON.stringify(map));
|
||||
}
|
||||
const data = localStorage.getItem('webhookEventsByRoom');
|
||||
if (!data) return;
|
||||
|
||||
const map = JSON.parse(data);
|
||||
if (map[roomId]) {
|
||||
map[roomId] = [];
|
||||
localStorage.setItem('webhookEventsByRoom', JSON.stringify(map));
|
||||
}
|
||||
};
|
||||
|
||||
const shouldShowWebhook = (event: any): boolean => {
|
||||
return showAllWebhooksCheckbox?.checked || event.data.roomId === roomId;
|
||||
return showAllWebhooksCheckbox?.checked || event.data.roomId === roomId;
|
||||
};
|
||||
|
||||
const listenWebhookServerEvents = () => {
|
||||
socket.on('webhookEvent', (event: any) => {
|
||||
console.log('Webhook received:', event);
|
||||
const webhookRoomId = event.data.roomId;
|
||||
socket.on('webhookEvent', (event: any) => {
|
||||
console.log('Webhook received:', event);
|
||||
const webhookRoomId = event.data.roomId;
|
||||
|
||||
if (webhookRoomId) {
|
||||
saveWebhookEventToStorage(webhookRoomId, event);
|
||||
}
|
||||
if (webhookRoomId) {
|
||||
saveWebhookEventToStorage(webhookRoomId, event);
|
||||
}
|
||||
|
||||
if (!shouldShowWebhook(event)) {
|
||||
console.log('Ignoring webhook event:', event);
|
||||
return;
|
||||
}
|
||||
if (!shouldShowWebhook(event)) {
|
||||
console.log('Ignoring webhook event:', event);
|
||||
return;
|
||||
}
|
||||
|
||||
addWebhookEventElement(event);
|
||||
addWebhookEventElement(event);
|
||||
|
||||
// Clean up the previous events
|
||||
const isMeetingEnded = event.event === 'meetingEnded';
|
||||
if (isMeetingEnded) clearWebhookEventsByRoom(webhookRoomId);
|
||||
});
|
||||
// Clean up the previous events
|
||||
const isMeetingEnded = event.event === 'meetingEnded';
|
||||
if (isMeetingEnded) clearWebhookEventsByRoom(webhookRoomId);
|
||||
});
|
||||
};
|
||||
|
||||
const renderStoredWebhookEvents = (roomId: string) => {
|
||||
const webhookLogList = document.getElementById('webhook-log-list');
|
||||
if (webhookLogList) {
|
||||
while (webhookLogList.firstChild) {
|
||||
webhookLogList.removeChild(webhookLogList.firstChild);
|
||||
}
|
||||
}
|
||||
const events = getWebhookEventsFromStorage(roomId);
|
||||
events.forEach((event) => addWebhookEventElement(event));
|
||||
const webhookLogList = document.getElementById('webhook-log-list');
|
||||
if (webhookLogList) {
|
||||
while (webhookLogList.firstChild) {
|
||||
webhookLogList.removeChild(webhookLogList.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
const events = getWebhookEventsFromStorage(roomId);
|
||||
events.forEach((event) => addWebhookEventElement(event));
|
||||
};
|
||||
|
||||
const addWebhookEventElement = (event: any) => {
|
||||
const webhookLogList = document.getElementById('webhook-log-list');
|
||||
if (webhookLogList) {
|
||||
// Create unique IDs for this accordion item
|
||||
const itemId = event.creationDate;
|
||||
const headerClassName = `webhook-${event.event}`;
|
||||
const collapseId = `collapse-${itemId}`;
|
||||
const webhookLogList = document.getElementById('webhook-log-list');
|
||||
if (webhookLogList) {
|
||||
// Create unique IDs for this accordion item
|
||||
const itemId = event.creationDate;
|
||||
const headerClassName = `webhook-${event.event}`;
|
||||
const collapseId = `collapse-${itemId}`;
|
||||
|
||||
// Create accordion item container
|
||||
const accordionItem = document.createElement('div');
|
||||
accordionItem.className = 'accordion-item';
|
||||
// Create accordion item container
|
||||
const accordionItem = document.createElement('div');
|
||||
accordionItem.className = 'accordion-item';
|
||||
|
||||
// Create header
|
||||
const header = document.createElement('h2');
|
||||
header.classList.add(headerClassName, 'accordion-header');
|
||||
// Create header
|
||||
const header = document.createElement('h2');
|
||||
header.classList.add(headerClassName, 'accordion-header');
|
||||
|
||||
// Create header button
|
||||
const button = document.createElement('button');
|
||||
button.className = 'accordion-button';
|
||||
button.type = 'button';
|
||||
button.setAttribute('data-bs-toggle', 'collapse');
|
||||
button.setAttribute('data-bs-target', `#${collapseId}`);
|
||||
button.setAttribute('aria-expanded', 'true');
|
||||
button.setAttribute('aria-controls', collapseId);
|
||||
button.style.padding = '10px';
|
||||
// Create header button
|
||||
const button = document.createElement('button');
|
||||
button.className = 'accordion-button';
|
||||
button.type = 'button';
|
||||
button.setAttribute('data-bs-toggle', 'collapse');
|
||||
button.setAttribute('data-bs-target', `#${collapseId}`);
|
||||
button.setAttribute('aria-expanded', 'true');
|
||||
button.setAttribute('aria-controls', collapseId);
|
||||
button.style.padding = '10px';
|
||||
|
||||
if (event.event === 'meetingStarted') {
|
||||
button.classList.add('bg-success');
|
||||
}
|
||||
if (event.event === 'meetingEnded') {
|
||||
button.classList.add('bg-danger');
|
||||
}
|
||||
if (event.event.includes('recording')) {
|
||||
button.classList.add('bg-warning');
|
||||
}
|
||||
// Format the header text with event name and timestamp
|
||||
const date = new Date(event.creationDate);
|
||||
if (event.event === 'meetingStarted') {
|
||||
button.classList.add('bg-success');
|
||||
}
|
||||
if (event.event === 'meetingEnded') {
|
||||
button.classList.add('bg-danger');
|
||||
}
|
||||
if (event.event.includes('recording')) {
|
||||
button.classList.add('bg-warning');
|
||||
}
|
||||
|
||||
const formattedDate = date.toLocaleString('es-ES', {
|
||||
// year: 'numeric',
|
||||
// month: '2-digit',
|
||||
// day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false,
|
||||
});
|
||||
button.innerHTML = `[${formattedDate}] <strong>${event.event}</strong>`;
|
||||
// Format the header text with event name and timestamp
|
||||
const date = new Date(event.creationDate);
|
||||
const formattedDate = date.toLocaleString('es-ES', {
|
||||
// year: 'numeric',
|
||||
// month: '2-digit',
|
||||
// day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit',
|
||||
hour12: false
|
||||
});
|
||||
button.innerHTML = `[${formattedDate}] <strong>${event.event}</strong>`;
|
||||
|
||||
// Create collapsible content container
|
||||
const collapseDiv = document.createElement('div');
|
||||
collapseDiv.id = collapseId;
|
||||
collapseDiv.className = 'accordion-collapse collapse';
|
||||
collapseDiv.setAttribute('aria-labelledby', headerClassName);
|
||||
collapseDiv.setAttribute('data-bs-parent', '#webhook-log-list');
|
||||
// Create collapsible content container
|
||||
const collapseDiv = document.createElement('div');
|
||||
collapseDiv.id = collapseId;
|
||||
collapseDiv.className = 'accordion-collapse collapse';
|
||||
collapseDiv.setAttribute('aria-labelledby', headerClassName);
|
||||
collapseDiv.setAttribute('data-bs-parent', '#webhook-log-list');
|
||||
|
||||
// Create body content
|
||||
const bodyDiv = document.createElement('div');
|
||||
bodyDiv.className = 'accordion-body';
|
||||
// Create body content
|
||||
const bodyDiv = document.createElement('div');
|
||||
bodyDiv.className = 'accordion-body';
|
||||
|
||||
// Format JSON with syntax highlighting if possible
|
||||
const formattedJson = JSON.stringify(event, null, 2);
|
||||
bodyDiv.innerHTML = `<pre class="mb-0"><code>${escapeHtml(
|
||||
formattedJson
|
||||
)}</code></pre>`;
|
||||
// Format JSON with syntax highlighting if possible
|
||||
const formattedJson = JSON.stringify(event, null, 2);
|
||||
bodyDiv.innerHTML = `<pre class="mb-0"><code>${escapeHtml(formattedJson)}</code></pre>`;
|
||||
|
||||
// Assemble the components
|
||||
header.appendChild(button);
|
||||
collapseDiv.appendChild(bodyDiv);
|
||||
accordionItem.appendChild(header);
|
||||
accordionItem.appendChild(collapseDiv);
|
||||
// Assemble the components
|
||||
header.appendChild(button);
|
||||
collapseDiv.appendChild(bodyDiv);
|
||||
accordionItem.appendChild(header);
|
||||
accordionItem.appendChild(collapseDiv);
|
||||
|
||||
// Insert at the top of the list (latest events first)
|
||||
if (webhookLogList.firstChild) {
|
||||
webhookLogList.insertBefore(accordionItem, webhookLogList.firstChild);
|
||||
} else {
|
||||
webhookLogList.appendChild(accordionItem);
|
||||
}
|
||||
// Insert at the top of the list (latest events first)
|
||||
if (webhookLogList.firstChild) {
|
||||
webhookLogList.insertBefore(accordionItem, webhookLogList.firstChild);
|
||||
} else {
|
||||
webhookLogList.appendChild(accordionItem);
|
||||
}
|
||||
|
||||
// Limit the number of items to prevent performance issues
|
||||
const maxItems = 50;
|
||||
while (webhookLogList.children.length > maxItems) {
|
||||
webhookLogList.removeChild(webhookLogList.lastChild!);
|
||||
}
|
||||
}
|
||||
// Limit the number of items to prevent performance issues
|
||||
const maxItems = 50;
|
||||
while (webhookLogList.children.length > maxItems) {
|
||||
webhookLogList.removeChild(webhookLogList.lastChild!);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Listen to events from openvidu-meet
|
||||
|
||||
const listenWebComponentEvents = () => {
|
||||
const meet = document.querySelector('openvidu-meet') as any;
|
||||
if (!meet) {
|
||||
console.error('openvidu-meet component not found');
|
||||
alert('openvidu-meet component not found in the DOM');
|
||||
return;
|
||||
}
|
||||
const meet = document.querySelector('openvidu-meet') as any;
|
||||
if (!meet) {
|
||||
console.error('openvidu-meet component not found');
|
||||
alert('openvidu-meet component not found in the DOM');
|
||||
return;
|
||||
}
|
||||
|
||||
meet.on('JOIN', (event: CustomEvent<any>) => {
|
||||
console.log('JOIN event received:', event);
|
||||
addEventToLog('JOIN', JSON.stringify(event));
|
||||
});
|
||||
meet.on('LEFT', (event: CustomEvent<any>) => {
|
||||
console.log('LEFT event received:', event);
|
||||
addEventToLog('LEFT', JSON.stringify(event));
|
||||
});
|
||||
|
||||
meet.on('MEETING_ENDED', (event: CustomEvent<any>) => {
|
||||
console.log('MEETING_ENDED event received:', event);
|
||||
addEventToLog('MEETING_ENDED', JSON.stringify(event));
|
||||
});
|
||||
meet.on('JOIN', (event: CustomEvent<any>) => {
|
||||
console.log('JOIN event received:', event);
|
||||
addEventToLog('JOIN', JSON.stringify(event));
|
||||
});
|
||||
meet.on('LEFT', (event: CustomEvent<any>) => {
|
||||
console.log('LEFT event received:', event);
|
||||
addEventToLog('LEFT', JSON.stringify(event));
|
||||
});
|
||||
meet.on('MEETING_ENDED', (event: CustomEvent<any>) => {
|
||||
console.log('MEETING_ENDED event received:', event);
|
||||
addEventToLog('MEETING_ENDED', JSON.stringify(event));
|
||||
});
|
||||
};
|
||||
|
||||
// Set up commands for the web component
|
||||
const setUpWebComponentCommands = () => {
|
||||
if (!meet) {
|
||||
console.error('openvidu-meet component not found');
|
||||
alert('openvidu-meet component not found in the DOM');
|
||||
return;
|
||||
}
|
||||
if (!meet) {
|
||||
console.error('openvidu-meet component not found');
|
||||
alert('openvidu-meet component not found in the DOM');
|
||||
return;
|
||||
}
|
||||
|
||||
// End meeting button click handler
|
||||
document
|
||||
.getElementById('end-meeting-btn')
|
||||
?.addEventListener('click', () => meet.endMeeting());
|
||||
// End meeting button click handler
|
||||
document.getElementById('end-meeting-btn')?.addEventListener('click', () => meet.endMeeting());
|
||||
|
||||
// Leave room button click handler
|
||||
document
|
||||
.getElementById('leave-room-btn')
|
||||
?.addEventListener('click', () => meet.leaveRoom());
|
||||
// Leave room button click handler
|
||||
document.getElementById('leave-room-btn')?.addEventListener('click', () => meet.leaveRoom());
|
||||
|
||||
// Toggle chat button click handler
|
||||
// document
|
||||
// .getElementById('toggle-chat-btn')
|
||||
// ?.addEventListener('click', () => meet.toggleChat());
|
||||
// Kick participant button click handler
|
||||
document.getElementById('kick-participant-btn')?.addEventListener('click', () => {
|
||||
const participantIdentity = (
|
||||
document.getElementById('participant-identity-input') as HTMLInputElement
|
||||
).value.trim();
|
||||
if (participantIdentity) {
|
||||
meet.kickParticipant(participantIdentity);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
roomId = document.getElementById('room-id')?.textContent?.trim();
|
||||
showAllWebhooksCheckbox = document.getElementById(
|
||||
'show-all-webhooks'
|
||||
) as HTMLInputElement;
|
||||
meet = document.querySelector('openvidu-meet') as any;
|
||||
roomId = document.getElementById('room-id')?.textContent?.trim();
|
||||
showAllWebhooksCheckbox = document.getElementById('show-all-webhooks') as HTMLInputElement;
|
||||
meet = document.querySelector('openvidu-meet') as any;
|
||||
|
||||
if (!roomId) {
|
||||
console.error('Room ID not found in the DOM');
|
||||
alert('Room ID not found in the DOM');
|
||||
return;
|
||||
}
|
||||
renderStoredWebhookEvents(roomId);
|
||||
listenWebhookServerEvents();
|
||||
listenWebComponentEvents();
|
||||
setUpWebComponentCommands();
|
||||
if (!roomId) {
|
||||
console.error('Room ID not found in the DOM');
|
||||
alert('Room ID not found in the DOM');
|
||||
return;
|
||||
}
|
||||
|
||||
showAllWebhooksCheckbox?.addEventListener('change', () => {
|
||||
if (roomId) renderStoredWebhookEvents(roomId);
|
||||
});
|
||||
renderStoredWebhookEvents(roomId);
|
||||
listenWebhookServerEvents();
|
||||
listenWebComponentEvents();
|
||||
setUpWebComponentCommands();
|
||||
|
||||
showAllWebhooksCheckbox?.addEventListener('change', () => {
|
||||
if (roomId) renderStoredWebhookEvents(roomId);
|
||||
});
|
||||
});
|
||||
|
||||
@ -109,24 +109,29 @@
|
||||
</button>
|
||||
</form>
|
||||
</li>
|
||||
{{! ONLY RECORDINGS LINK }}
|
||||
{{! SHOW ONLY RECORDINGS LINK }}
|
||||
<li>
|
||||
<form action="/join-room" method="post">
|
||||
<input
|
||||
type="hidden"
|
||||
name="roomUrl"
|
||||
value="{{ publisherRoomUrl + '?viewRecordings=true' }}"
|
||||
value="{{ moderatorRoomUrl }}"
|
||||
/>
|
||||
<input
|
||||
type="hidden"
|
||||
name="participantRole"
|
||||
value="publisher"
|
||||
value="moderator"
|
||||
/>
|
||||
<input type="hidden" name="roomId" value="{{ roomId }}" />
|
||||
<input
|
||||
type="hidden"
|
||||
name="showOnlyRecordings"
|
||||
value="true"
|
||||
/>
|
||||
|
||||
<button
|
||||
type="submit"
|
||||
id="join-as-publisher"
|
||||
id="view-recordings"
|
||||
class="dropdown-item"
|
||||
>
|
||||
View Recordings
|
||||
@ -273,7 +278,6 @@
|
||||
<option value="admin-moderator-publisher" selected>
|
||||
Admin, Moderator & Publisher
|
||||
</option>
|
||||
<option value="public">Public Access</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -42,9 +42,17 @@
|
||||
<button id="leave-room-btn" class="btn btn-warning">
|
||||
Leave Room
|
||||
</button>
|
||||
<!-- <button id="toggle-chat-btn" class="btn btn-success">
|
||||
Toggle Chat
|
||||
</button> -->
|
||||
{{#isModerator}}
|
||||
<input
|
||||
type="text"
|
||||
id="participant-identity-input"
|
||||
placeholder="Participant Identity"
|
||||
class="form-control mb-2"
|
||||
/>
|
||||
<button id="kick-participant-btn" class="btn btn-info">
|
||||
Kick Participant
|
||||
</button>
|
||||
{{/isModerator}}
|
||||
</div>
|
||||
|
||||
<!-- Events -->
|
||||
@ -82,7 +90,9 @@
|
||||
<openvidu-meet
|
||||
room-url="{{ roomUrl }}"
|
||||
participant-name="{{ participantName }}"
|
||||
leave-redirect-url="https://openvidu.io"
|
||||
{{#showOnlyRecordings}}
|
||||
show-only-recordings=true
|
||||
{{/showOnlyRecordings}}
|
||||
></openvidu-meet>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@ -1,124 +1,112 @@
|
||||
import { Request, Response } from 'express';
|
||||
import {
|
||||
getAllRooms,
|
||||
createRoom,
|
||||
deleteRoom,
|
||||
deleteAllRooms,
|
||||
} from '../services/roomService';
|
||||
import {
|
||||
deleteAllRecordings,
|
||||
getAllRecordings,
|
||||
} from '../services/recordingService';
|
||||
import { getAllRooms, createRoom, deleteRoom, deleteAllRooms } from '../services/roomService';
|
||||
import { deleteAllRecordings, getAllRecordings } from '../services/recordingService';
|
||||
|
||||
export const getHome = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { rooms } = await getAllRooms();
|
||||
export const getHome = async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const { rooms } = await getAllRooms();
|
||||
|
||||
//sort rooms by newest first
|
||||
rooms.sort((a, b) => {
|
||||
const dateA = new Date(a.creationDate);
|
||||
const dateB = new Date(b.creationDate);
|
||||
return dateB.getTime() - dateA.getTime();
|
||||
});
|
||||
// Sort rooms by newest first
|
||||
rooms.sort((a, b) => {
|
||||
const dateA = new Date(a.creationDate);
|
||||
const dateB = new Date(b.creationDate);
|
||||
return dateB.getTime() - dateA.getTime();
|
||||
});
|
||||
|
||||
console.log(`Rooms fetched: ${rooms.length}`);
|
||||
res.render('index', { rooms });
|
||||
} catch (error) {
|
||||
console.error('Error fetching rooms:', error);
|
||||
res.status(500).send('Internal Server Error');
|
||||
return;
|
||||
}
|
||||
console.log(`Rooms fetched: ${rooms.length}`);
|
||||
res.render('index', { rooms });
|
||||
} catch (error) {
|
||||
console.error('Error fetching rooms:', error);
|
||||
res.status(500).send('Internal Server Error');
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export const postCreateRoom = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { roomIdPrefix, autoDeletionDate } = req.body;
|
||||
const preferences = processFormPreferences(req.body);
|
||||
try {
|
||||
const { roomIdPrefix, autoDeletionDate } = req.body;
|
||||
const preferences = processFormPreferences(req.body);
|
||||
|
||||
await createRoom({ roomIdPrefix, autoDeletionDate, preferences });
|
||||
res.redirect('/');
|
||||
} catch (error) {
|
||||
console.error('Error creating room:', error);
|
||||
res.status(500).send('Internal Server Error');
|
||||
return;
|
||||
}
|
||||
await createRoom({ roomIdPrefix, autoDeletionDate, preferences });
|
||||
res.redirect('/');
|
||||
} catch (error) {
|
||||
console.error('Error creating room:', error);
|
||||
res.status(500).send('Internal Server Error');
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteRoomCtrl = async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { roomId } = req.body;
|
||||
await deleteRoom(roomId);
|
||||
res.redirect('/');
|
||||
} catch (error) {
|
||||
console.error('Error deleting room:', error);
|
||||
res.status(500).send('Internal Server Error');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { roomId } = req.body;
|
||||
await deleteRoom(roomId);
|
||||
res.redirect('/');
|
||||
} catch (error) {
|
||||
console.error('Error deleting room:', error);
|
||||
res.status(500).send('Internal Server Error');
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteAllRoomsCtrl = async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const allRooms = await getAllRooms();
|
||||
if (allRooms.rooms.length === 0) {
|
||||
console.log('No rooms to delete');
|
||||
res.render('index', { rooms: [] });
|
||||
return;
|
||||
}
|
||||
const roomIds = allRooms.rooms.map((room) => room.roomId);
|
||||
console.log(`Deleting ${roomIds.length} rooms`, roomIds);
|
||||
await deleteAllRooms(roomIds);
|
||||
res.render('index', { rooms: [] });
|
||||
} catch (error) {
|
||||
console.error('Error deleting all rooms:', error);
|
||||
res.status(500).send('Internal Server Error ' + JSON.stringify(error));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const allRooms = await getAllRooms();
|
||||
if (allRooms.rooms.length === 0) {
|
||||
console.log('No rooms to delete');
|
||||
res.render('index', { rooms: [] });
|
||||
return;
|
||||
}
|
||||
|
||||
const roomIds = allRooms.rooms.map((room) => room.roomId);
|
||||
console.log(`Deleting ${roomIds.length} rooms`, roomIds);
|
||||
await deleteAllRooms(roomIds);
|
||||
res.render('index', { rooms: [] });
|
||||
} catch (error) {
|
||||
console.error('Error deleting all rooms:', error);
|
||||
res.status(500).send('Internal Server Error ' + JSON.stringify(error));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export const deleteAllRecordingsCtrl = async (_req: Request, res: Response) => {
|
||||
try {
|
||||
const [{ recordings }, { rooms }] = await Promise.all([
|
||||
getAllRecordings(),
|
||||
getAllRooms(),
|
||||
]);
|
||||
if (recordings.length === 0) {
|
||||
console.log('No recordings to delete');
|
||||
res.render('index', { rooms });
|
||||
return;
|
||||
}
|
||||
const recordingIds = recordings.map((recording) => recording.recordingId);
|
||||
await deleteAllRecordings(recordingIds);
|
||||
console.log(`Deleted ${recordingIds.length} recordings`);
|
||||
res.render('index', { rooms });
|
||||
} catch (error) {
|
||||
console.error('Error deleting all recordings:', error);
|
||||
res.status(500).send('Internal Server Error ' + JSON.stringify(error));
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const [{ recordings }, { rooms }] = await Promise.all([getAllRecordings(), getAllRooms()]);
|
||||
if (recordings.length === 0) {
|
||||
console.log('No recordings to delete');
|
||||
res.render('index', { rooms });
|
||||
return;
|
||||
}
|
||||
|
||||
const recordingIds = recordings.map((recording) => recording.recordingId);
|
||||
await deleteAllRecordings(recordingIds);
|
||||
console.log(`Deleted ${recordingIds.length} recordings`);
|
||||
res.render('index', { rooms });
|
||||
} catch (error) {
|
||||
console.error('Error deleting all recordings:', error);
|
||||
res.status(500).send('Internal Server Error ' + JSON.stringify(error));
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts flat form data to nested MeetRoomPreferences object
|
||||
*/
|
||||
const processFormPreferences = (body: any): any => {
|
||||
const preferences = {
|
||||
chatPreferences: {
|
||||
enabled: body['preferences.chatPreferences.enabled'] === 'on',
|
||||
},
|
||||
recordingPreferences: {
|
||||
enabled: body['preferences.recordingPreferences.enabled'] === 'on',
|
||||
// Only include allowAccessTo if recording is enabled
|
||||
...(body['preferences.recordingPreferences.enabled'] === 'on' && {
|
||||
allowAccessTo:
|
||||
body['preferences.recordingPreferences.allowAccessTo'] ||
|
||||
'admin-moderator-publisher',
|
||||
}),
|
||||
},
|
||||
virtualBackgroundPreferences: {
|
||||
enabled:
|
||||
body['preferences.virtualBackgroundPreferences.enabled'] === 'on',
|
||||
},
|
||||
};
|
||||
const preferences = {
|
||||
chatPreferences: {
|
||||
enabled: body['preferences.chatPreferences.enabled'] === 'on'
|
||||
},
|
||||
recordingPreferences: {
|
||||
enabled: body['preferences.recordingPreferences.enabled'] === 'on',
|
||||
// Only include allowAccessTo if recording is enabled
|
||||
...(body['preferences.recordingPreferences.enabled'] === 'on' && {
|
||||
allowAccessTo: body['preferences.recordingPreferences.allowAccessTo'] || 'admin-moderator-publisher'
|
||||
})
|
||||
},
|
||||
virtualBackgroundPreferences: {
|
||||
enabled: body['preferences.virtualBackgroundPreferences.enabled'] === 'on'
|
||||
}
|
||||
};
|
||||
|
||||
return preferences;
|
||||
return preferences;
|
||||
};
|
||||
|
||||
@ -5,56 +5,55 @@ import { ParticipantRole } from '../../../typings/src/participant';
|
||||
import { MeetWebhookEvent } from '../../../typings/src/webhook.model';
|
||||
|
||||
interface JoinRoomRequest {
|
||||
participantRole: ParticipantRole;
|
||||
roomUrl: string;
|
||||
roomId: string;
|
||||
participantName?: string;
|
||||
participantRole: ParticipantRole;
|
||||
roomUrl: string;
|
||||
roomId: string;
|
||||
participantName?: string;
|
||||
showOnlyRecordings?: boolean;
|
||||
}
|
||||
|
||||
export const joinRoom = (req: Request, res: Response) => {
|
||||
try {
|
||||
const {
|
||||
participantRole,
|
||||
roomUrl,
|
||||
roomId,
|
||||
participantName = 'User',
|
||||
} = req.body as JoinRoomRequest;
|
||||
if (!roomUrl) {
|
||||
throw new Error('Room URL is required.');
|
||||
}
|
||||
res.render('room', {
|
||||
roomUrl,
|
||||
participantRole,
|
||||
participantName,
|
||||
roomId,
|
||||
isModerator: participantRole === 'moderator',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error joining room:', error);
|
||||
res.status(500).send('Internal Server Error');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const {
|
||||
participantRole,
|
||||
roomUrl,
|
||||
roomId,
|
||||
participantName = 'User',
|
||||
showOnlyRecordings
|
||||
} = req.body as JoinRoomRequest;
|
||||
if (!roomUrl) {
|
||||
throw new Error('Room URL is required.');
|
||||
}
|
||||
|
||||
res.render('room', {
|
||||
roomUrl,
|
||||
isModerator: participantRole === 'moderator',
|
||||
participantName,
|
||||
roomId,
|
||||
showOnlyRecordings: showOnlyRecordings || false
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error joining room:', error);
|
||||
res.status(500).send('Internal Server Error');
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
export const handleWebhook = async (
|
||||
req: Request,
|
||||
res: Response,
|
||||
io: IOServer
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const webhookEvent = req.body as MeetWebhookEvent;
|
||||
export const handleWebhook = async (req: Request, res: Response, io: IOServer): Promise<void> => {
|
||||
try {
|
||||
const webhookEvent = req.body as MeetWebhookEvent;
|
||||
|
||||
// Log event details
|
||||
console.info(`Webhook received: ${webhookEvent.event}`);
|
||||
// Log event details
|
||||
console.info(`Webhook received: ${webhookEvent.event}`);
|
||||
|
||||
// Broadcast to all connected clients
|
||||
io.emit('webhookEvent', webhookEvent);
|
||||
res.status(200).send('Webhook received');
|
||||
} catch (error) {
|
||||
console.error('Error handling webhook:', error);
|
||||
res.status(400).json({
|
||||
message: 'Error handling webhook',
|
||||
error: (error as Error).message,
|
||||
});
|
||||
}
|
||||
// Broadcast to all connected clients
|
||||
io.emit('webhookEvent', webhookEvent);
|
||||
res.status(200).send('Webhook received');
|
||||
} catch (error) {
|
||||
console.error('Error handling webhook:', error);
|
||||
res.status(400).json({
|
||||
message: 'Error handling webhook',
|
||||
error: (error as Error).message
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -1,13 +1,13 @@
|
||||
import express from 'express';
|
||||
import http from 'http';
|
||||
import { Server as IOServer } from 'socket.io';
|
||||
import path from 'path';
|
||||
import { Server as IOServer } from 'socket.io';
|
||||
import {
|
||||
getHome,
|
||||
postCreateRoom,
|
||||
deleteRoomCtrl,
|
||||
deleteAllRoomsCtrl,
|
||||
deleteAllRecordingsCtrl,
|
||||
deleteAllRecordingsCtrl,
|
||||
deleteAllRoomsCtrl,
|
||||
deleteRoomCtrl,
|
||||
getHome,
|
||||
postCreateRoom
|
||||
} from './controllers/homeController';
|
||||
import { handleWebhook, joinRoom } from './controllers/roomController';
|
||||
import { configService } from './services/configService';
|
||||
@ -31,27 +31,24 @@ app.use(express.json());
|
||||
|
||||
// Routes
|
||||
app.get('/', getHome);
|
||||
app.get('/room', joinRoom);
|
||||
app.post('/room', postCreateRoom);
|
||||
app.post('/room/delete', deleteRoomCtrl);
|
||||
app.post('/delete-all-rooms', deleteAllRoomsCtrl);
|
||||
app.post('/delete-all-recordings', deleteAllRecordingsCtrl);
|
||||
app.post('/join-room', joinRoom);
|
||||
app.post('/webhook', (req, res) => {
|
||||
handleWebhook(req, res, io);
|
||||
handleWebhook(req, res, io);
|
||||
});
|
||||
|
||||
const PORT = configService.serverPort;
|
||||
server.listen(PORT, () => {
|
||||
console.log('-----------------------------------------');
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
console.log(`Meet API URL: ${configService.meetApiUrl}`);
|
||||
console.log('-----------------------------------------');
|
||||
console.log('');
|
||||
console.log(`Visit http://localhost:${PORT}/ to access the app`);
|
||||
console.log('-----------------------------------------');
|
||||
console.log(`Server running on port ${PORT}`);
|
||||
console.log(`Visit http://localhost:${PORT}/ to access the app`);
|
||||
console.log('-----------------------------------------');
|
||||
console.log('');
|
||||
|
||||
console.log('Environment variables:');
|
||||
console.log(`OPENVIDU_MEET_URL: ${process.env.OPENVIDU_MEET_URL}`);
|
||||
console.log(`MEET_API_KEY: ${process.env.MEET_API_KEY} `);
|
||||
console.log(`PORT: ${process.env.PORT}`);
|
||||
console.log('OpenVidu Meet Configuration:');
|
||||
console.log(`Meet API URL: ${configService.meetApiUrl}`);
|
||||
console.log(`Meet API key: ${configService.meetApiKey}`);
|
||||
});
|
||||
|
||||
@ -3,15 +3,15 @@ import dotenv from 'dotenv';
|
||||
dotenv.config();
|
||||
|
||||
export class ConfigService {
|
||||
public meetApiUrl: string;
|
||||
public meetApiKey: string;
|
||||
public serverPort: number;
|
||||
public meetApiUrl: string;
|
||||
public meetApiKey: string;
|
||||
public serverPort: number;
|
||||
|
||||
constructor() {
|
||||
this.meetApiUrl = process.env.OPENVIDU_MEET_URL!;
|
||||
this.meetApiKey = process.env.MEET_API_KEY!;
|
||||
this.serverPort = parseInt(process.env.PORT!, 10);
|
||||
}
|
||||
constructor() {
|
||||
this.meetApiUrl = process.env.OPENVIDU_MEET_URL!;
|
||||
this.meetApiKey = process.env.MEET_API_KEY!;
|
||||
this.serverPort = parseInt(process.env.PORT!, 10);
|
||||
}
|
||||
}
|
||||
|
||||
export const configService = new ConfigService();
|
||||
|
||||
@ -1,41 +1,39 @@
|
||||
import { del, get } from '../utils/http';
|
||||
// @ts-ignore
|
||||
import { MeetRecordingInfo } from '../../../typings/src/recording.model';
|
||||
import { configService } from './configService';
|
||||
|
||||
export const getAllRecordings = async (): Promise<{
|
||||
pagination: { isTruncated: boolean; nextPageToken?: string };
|
||||
recordings: MeetRecordingInfo[];
|
||||
pagination: { isTruncated: boolean; nextPageToken?: string };
|
||||
recordings: MeetRecordingInfo[];
|
||||
}> => {
|
||||
const url = `${configService.meetApiUrl}/recordings`;
|
||||
const url = `${configService.meetApiUrl}/recordings`;
|
||||
|
||||
let { pagination, recordings } = await get<{
|
||||
pagination: any;
|
||||
recordings: MeetRecordingInfo[];
|
||||
}>(url, {
|
||||
headers: { 'x-api-key': configService.meetApiKey },
|
||||
});
|
||||
let { pagination, recordings } = await get<{
|
||||
pagination: any;
|
||||
recordings: MeetRecordingInfo[];
|
||||
}>(url, {
|
||||
headers: { 'x-api-key': configService.meetApiKey }
|
||||
});
|
||||
|
||||
while (pagination.isTruncated) {
|
||||
const nextPageUrl = `${url}?nextPageToken=${pagination.nextPageToken}`;
|
||||
const nextPageResult = await get<{
|
||||
pagination: any;
|
||||
recordings: MeetRecordingInfo[];
|
||||
}>(nextPageUrl, {
|
||||
headers: { 'x-api-key': configService.meetApiKey },
|
||||
});
|
||||
recordings.push(...nextPageResult.recordings);
|
||||
pagination = nextPageResult.pagination;
|
||||
}
|
||||
return { pagination, recordings };
|
||||
while (pagination.isTruncated) {
|
||||
const nextPageUrl = `${url}?nextPageToken=${pagination.nextPageToken}`;
|
||||
const nextPageResult = await get<{
|
||||
pagination: any;
|
||||
recordings: MeetRecordingInfo[];
|
||||
}>(nextPageUrl, {
|
||||
headers: { 'x-api-key': configService.meetApiKey }
|
||||
});
|
||||
recordings.push(...nextPageResult.recordings);
|
||||
pagination = nextPageResult.pagination;
|
||||
}
|
||||
|
||||
return { pagination, recordings };
|
||||
};
|
||||
|
||||
export const deleteAllRecordings = async (
|
||||
recordingIds: string[]
|
||||
): Promise<void> => {
|
||||
const url = `${
|
||||
configService.meetApiUrl
|
||||
}/recordings?recordingIds=${recordingIds.join(',')}`;
|
||||
await del<void>(url, {
|
||||
headers: { 'x-api-key': configService.meetApiKey },
|
||||
});
|
||||
export const deleteAllRecordings = async (recordingIds: string[]): Promise<void> => {
|
||||
const url = `${configService.meetApiUrl}/recordings?recordingIds=${recordingIds.join(',')}`;
|
||||
await del<void>(url, {
|
||||
headers: { 'x-api-key': configService.meetApiKey }
|
||||
});
|
||||
};
|
||||
|
||||
@ -1,41 +1,45 @@
|
||||
import { get, post, del } from '../utils/http';
|
||||
import { del, get, post } from '../utils/http';
|
||||
import { configService } from './configService';
|
||||
// @ts-ignore
|
||||
import { MeetRoom, MeetRoomOptions } from '../../../typings/src/room';
|
||||
|
||||
export async function getAllRooms(): Promise<{
|
||||
pagination: any;
|
||||
rooms: MeetRoom[];
|
||||
pagination: any;
|
||||
rooms: MeetRoom[];
|
||||
}> {
|
||||
const url = `${configService.meetApiUrl}/rooms`;
|
||||
return get<{ pagination: any; rooms: MeetRoom[] }>(url, {
|
||||
headers: { 'x-api-key': configService.meetApiKey },
|
||||
});
|
||||
const url = `${configService.meetApiUrl}/rooms`;
|
||||
return get<{ pagination: any; rooms: MeetRoom[] }>(url, {
|
||||
headers: { 'x-api-key': configService.meetApiKey }
|
||||
});
|
||||
}
|
||||
|
||||
export async function createRoom(roomData: MeetRoomOptions): Promise<MeetRoom> {
|
||||
const url = `${configService.meetApiUrl}/rooms`;
|
||||
// Default to 1 hour if autoDeletionDate is not provided
|
||||
if (!roomData.autoDeletionDate)
|
||||
roomData.autoDeletionDate = new Date(Date.now() + 60 * 61 * 1000).getTime();
|
||||
console.log('Creating room with options:', roomData);
|
||||
return post<MeetRoom>(url, {
|
||||
headers: { 'x-api-key': configService.meetApiKey },
|
||||
body: roomData,
|
||||
});
|
||||
const url = `${configService.meetApiUrl}/rooms`;
|
||||
// Default to 1 hour if autoDeletionDate is not provided
|
||||
if (!roomData.autoDeletionDate) {
|
||||
roomData.autoDeletionDate = new Date(Date.now() + 60 * 61 * 1000).getTime();
|
||||
} else {
|
||||
// Ensure autoDeletionDate is a timestamp
|
||||
roomData.autoDeletionDate = new Date(roomData.autoDeletionDate).getTime();
|
||||
}
|
||||
|
||||
console.log('Creating room with options:', roomData);
|
||||
return post<MeetRoom>(url, {
|
||||
headers: { 'x-api-key': configService.meetApiKey },
|
||||
body: roomData
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteRoom(roomId: string): Promise<void> {
|
||||
const url = `${configService.meetApiUrl}/rooms/${roomId}`;
|
||||
await del<void>(url, {
|
||||
headers: { 'x-api-key': configService.meetApiKey },
|
||||
});
|
||||
const url = `${configService.meetApiUrl}/rooms/${roomId}`;
|
||||
await del<void>(url, {
|
||||
headers: { 'x-api-key': configService.meetApiKey }
|
||||
});
|
||||
}
|
||||
|
||||
export async function deleteAllRooms(roomIds: string[]): Promise<void> {
|
||||
const url = `${configService.meetApiUrl}/rooms?roomIds=${roomIds.join(',')}`;
|
||||
|
||||
await del<void>(url, {
|
||||
headers: { 'x-api-key': configService.meetApiKey },
|
||||
});
|
||||
const url = `${configService.meetApiUrl}/rooms?roomIds=${roomIds.join(',')}`;
|
||||
await del<void>(url, {
|
||||
headers: { 'x-api-key': configService.meetApiKey }
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,70 +1,50 @@
|
||||
export interface RequestOptions {
|
||||
headers?: Record<string, string>;
|
||||
queryParams?: Record<string, string>;
|
||||
body?: any;
|
||||
headers?: Record<string, string>;
|
||||
queryParams?: Record<string, string>;
|
||||
body?: any;
|
||||
}
|
||||
|
||||
async function buildUrl(
|
||||
url: string,
|
||||
queryParams?: Record<string, string>
|
||||
): Promise<string> {
|
||||
if (!queryParams) return url;
|
||||
const params = new URLSearchParams(
|
||||
queryParams as Record<string, string>
|
||||
).toString();
|
||||
return `${url}?${params}`;
|
||||
async function buildUrl(url: string, queryParams?: Record<string, string>): Promise<string> {
|
||||
if (!queryParams) return url;
|
||||
const params = new URLSearchParams(queryParams as Record<string, string>).toString();
|
||||
return `${url}?${params}`;
|
||||
}
|
||||
|
||||
async function request<T>(
|
||||
method: string,
|
||||
url: string,
|
||||
options: RequestOptions = {}
|
||||
): Promise<T> {
|
||||
const fullUrl = await buildUrl(url, options.queryParams);
|
||||
const fetchOptions: RequestInit = {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json', ...(options.headers || {}) },
|
||||
body: options.body ? JSON.stringify(options.body) : undefined,
|
||||
};
|
||||
const response = await fetch(fullUrl, fetchOptions);
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(`HTTP ${response.status}: ${text}`);
|
||||
}
|
||||
async function request<T>(method: string, url: string, options: RequestOptions = {}): Promise<T> {
|
||||
const fullUrl = await buildUrl(url, options.queryParams);
|
||||
const fetchOptions: RequestInit = {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json', ...(options.headers || {}) },
|
||||
body: options.body ? JSON.stringify(options.body) : undefined
|
||||
};
|
||||
|
||||
// Handle empty responses (e.g., for DELETE requests)
|
||||
const text = await response.text();
|
||||
if (!text) {
|
||||
return {} as T;
|
||||
}
|
||||
const response = await fetch(fullUrl, fetchOptions);
|
||||
if (!response.ok) {
|
||||
const text = await response.text();
|
||||
throw new Error(`HTTP ${response.status}: ${text}`);
|
||||
}
|
||||
|
||||
return JSON.parse(text) as T;
|
||||
// Handle empty responses (e.g., for DELETE requests)
|
||||
const text = await response.text();
|
||||
if (!text) {
|
||||
return {} as T;
|
||||
}
|
||||
|
||||
return JSON.parse(text) as T;
|
||||
}
|
||||
|
||||
export function get<T>(
|
||||
url: string,
|
||||
options?: Omit<RequestOptions, 'body'>
|
||||
): Promise<T> {
|
||||
return request<T>('GET', url, options || {});
|
||||
export function get<T>(url: string, options?: Omit<RequestOptions, 'body'>): Promise<T> {
|
||||
return request<T>('GET', url, options || {});
|
||||
}
|
||||
|
||||
export function post<T>(
|
||||
url: string,
|
||||
options?: Omit<RequestOptions, 'body'> & { body: any }
|
||||
): Promise<T> {
|
||||
return request<T>('POST', url, options as RequestOptions);
|
||||
export function post<T>(url: string, options?: Omit<RequestOptions, 'body'> & { body: any }): Promise<T> {
|
||||
return request<T>('POST', url, options as RequestOptions);
|
||||
}
|
||||
|
||||
export function put<T>(
|
||||
url: string,
|
||||
options?: Omit<RequestOptions, 'body'> & { body: any }
|
||||
): Promise<T> {
|
||||
return request<T>('PUT', url, options as RequestOptions);
|
||||
export function put<T>(url: string, options?: Omit<RequestOptions, 'body'> & { body: any }): Promise<T> {
|
||||
return request<T>('PUT', url, options as RequestOptions);
|
||||
}
|
||||
|
||||
export function del<T>(
|
||||
url: string,
|
||||
options?: Omit<RequestOptions, 'body'>
|
||||
): Promise<T> {
|
||||
return request<T>('DELETE', url, options || {});
|
||||
export function del<T>(url: string, options?: Omit<RequestOptions, 'body'>): Promise<T> {
|
||||
return request<T>('DELETE', url, options || {});
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user