From 0b09cff0597d88ad452710464696eb7e70d95cb9 Mon Sep 17 00:00:00 2001 From: akramhany Date: Thu, 12 Dec 2024 00:07:59 +0200 Subject: [PATCH 01/54] feat(voice-calls): change the voice calls model and type --- src/models/voiceCallModel.ts | 18 +++++++++++------- src/types/voiceCall.ts | 6 ++++-- 2 files changed, 15 insertions(+), 9 deletions(-) diff --git a/src/models/voiceCallModel.ts b/src/models/voiceCallModel.ts index acf6d77..4077a03 100644 --- a/src/models/voiceCallModel.ts +++ b/src/models/voiceCallModel.ts @@ -3,14 +3,18 @@ import mongoose from 'mongoose'; import Communication from './communicationModel'; const voiceCallSchema = new mongoose.Schema({ - duration: { - type: Number, - default: 0, - }, - status: { - type: String, - enum: ['declined', 'answered'], + timestamp: { type: Date, required: true }, + duration: { type: Number }, + callType: { type: String, enum: ['group', 'private'], required: true }, + senderId: { + type: mongoose.Schema.Types.ObjectId, + ref: 'User', + required: true, }, + participants: [ + { type: mongoose.Types.ObjectId, ref: 'User', required: true, default: [] }, + ], + chatId: { type: mongoose.Schema.Types.ObjectId, ref: 'Chat', required: true }, }); const VoiceCall = Communication.discriminator('VoiceCall', voiceCallSchema); diff --git a/src/types/voiceCall.ts b/src/types/voiceCall.ts index d8c0776..1fd5cbc 100644 --- a/src/types/voiceCall.ts +++ b/src/types/voiceCall.ts @@ -1,8 +1,10 @@ +import { Types } from 'mongoose'; import ICommunication from './communication'; interface IVoiceCall extends ICommunication { - duration: number; - status: String; + callType: String; + participants: Types.ObjectId[]; + duration: Number; } export default IVoiceCall; From 4ddb10638fecf2ff3fa8c393ba014271f556cf7f Mon Sep 17 00:00:00 2001 From: akramhany Date: Thu, 12 Dec 2024 00:47:50 +0200 Subject: [PATCH 02/54] feat(voice-calls): update voice call model and type --- src/models/voiceCallModel.ts | 3 ++- src/types/voiceCall.ts | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/models/voiceCallModel.ts b/src/models/voiceCallModel.ts index 4077a03..f81b29b 100644 --- a/src/models/voiceCallModel.ts +++ b/src/models/voiceCallModel.ts @@ -6,12 +6,13 @@ const voiceCallSchema = new mongoose.Schema({ timestamp: { type: Date, required: true }, duration: { type: Number }, callType: { type: String, enum: ['group', 'private'], required: true }, + status: { type: String, enum: ['ongoing', 'finished'], required: true }, senderId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true, }, - participants: [ + currentParticipants: [ { type: mongoose.Types.ObjectId, ref: 'User', required: true, default: [] }, ], chatId: { type: mongoose.Schema.Types.ObjectId, ref: 'Chat', required: true }, diff --git a/src/types/voiceCall.ts b/src/types/voiceCall.ts index 1fd5cbc..62babb2 100644 --- a/src/types/voiceCall.ts +++ b/src/types/voiceCall.ts @@ -3,8 +3,9 @@ import ICommunication from './communication'; interface IVoiceCall extends ICommunication { callType: String; - participants: Types.ObjectId[]; + currentParticipants: Types.ObjectId[]; duration: Number; + status: String; } export default IVoiceCall; From f5e97209fdd0e5e33e090505770d203ca260ce4f Mon Sep 17 00:00:00 2001 From: akramhany Date: Thu, 12 Dec 2024 01:31:24 +0200 Subject: [PATCH 03/54] feat(voice-calls): make the initial structure of voice calls --- src/sockets/socket.ts | 2 + src/sockets/voiceCalls.ts | 77 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 79 insertions(+) create mode 100644 src/sockets/voiceCalls.ts diff --git a/src/sockets/socket.ts b/src/sockets/socket.ts index a851bc9..46b8c90 100644 --- a/src/sockets/socket.ts +++ b/src/sockets/socket.ts @@ -19,6 +19,7 @@ import { } from './services'; import registerMessagesHandlers from './messages'; import { authorizeSocket, protectSocket } from './middlewares'; +import registerVoiceCallHandlers from './voiceCalls'; const socketSetup = (server: HTTPServer) => { const io = new Server(server, { @@ -96,6 +97,7 @@ const socketSetup = (server: HTTPServer) => { registerChatHandlers(io, socket, userId); registerMessagesHandlers(io, socket, userId); + registerVoiceCallHandlers(io, socket, userId); }); }; diff --git a/src/sockets/voiceCalls.ts b/src/sockets/voiceCalls.ts new file mode 100644 index 0000000..7ae47a7 --- /dev/null +++ b/src/sockets/voiceCalls.ts @@ -0,0 +1,77 @@ +import { Server, Socket } from 'socket.io'; + +interface CreateCallData { + chatId: string; + targetId: string | undefined; +} + +interface JoinLeaveCallData { + voiceCallId: string; +} + +interface SignalData { + type: 'ICE' | 'OFFER' | 'ANSWER'; + targetId: string; + voiceCallId: string; + data: any; +} + +async function handleCreateCall( + io: Server, + socket: Socket, + data: CreateCallData, + userId: string +) { + console.log('Inside Create Call'); +} + +async function handleJoinCall( + io: Server, + socket: Socket, + data: JoinLeaveCallData, + userId: string +) { + console.log('Inside Join Call'); +} + +async function handleSignal( + io: Server, + socket: Socket, + data: SignalData, + userId: string +) { + console.log('Inside Signal'); +} + +async function handleLeaveCall( + io: Server, + socket: Socket, + data: JoinLeaveCallData, + userId: string +) { + console.log('Inside Leave Call'); +} + +async function registerVoiceCallHandlers( + io: Server, + socket: Socket, + userId: string +) { + socket.on('CREATE-CALL', (data: CreateCallData) => { + handleCreateCall(io, socket, data, userId); + }); + + socket.on('JOIN-CALL', (data: JoinLeaveCallData) => { + handleJoinCall(io, socket, data, userId); + }); + + socket.on('SIGNAL', (data: SignalData) => { + handleSignal(io, socket, data, userId); + }); + + socket.on('LEAVE', (data: JoinLeaveCallData) => { + handleLeaveCall(io, socket, data, userId); + }); +} + +export default registerVoiceCallHandlers; From 8703430a63a5fdb06126c1a2e21f2008f42393f0 Mon Sep 17 00:00:00 2001 From: akramhany Date: Thu, 12 Dec 2024 02:12:03 +0200 Subject: [PATCH 04/54] feat(voice-calls): implement CREATE-CALL event handler --- src/models/voiceCallModel.ts | 8 ++++---- src/sockets/voiceCalls.ts | 21 ++++++++++++++++++++- src/sockets/voiceCallsServices.ts | 17 +++++++++++++++++ 3 files changed, 41 insertions(+), 5 deletions(-) create mode 100644 src/sockets/voiceCallsServices.ts diff --git a/src/models/voiceCallModel.ts b/src/models/voiceCallModel.ts index f81b29b..16e894e 100644 --- a/src/models/voiceCallModel.ts +++ b/src/models/voiceCallModel.ts @@ -3,17 +3,17 @@ import mongoose from 'mongoose'; import Communication from './communicationModel'; const voiceCallSchema = new mongoose.Schema({ - timestamp: { type: Date, required: true }, - duration: { type: Number }, + timestamp: { type: Date, default: Date.now }, + duration: { type: Number, default: -1 }, callType: { type: String, enum: ['group', 'private'], required: true }, - status: { type: String, enum: ['ongoing', 'finished'], required: true }, + status: { type: String, enum: ['ongoing', 'finished'], default: 'ongoing' }, senderId: { type: mongoose.Schema.Types.ObjectId, ref: 'User', required: true, }, currentParticipants: [ - { type: mongoose.Types.ObjectId, ref: 'User', required: true, default: [] }, + { type: mongoose.Types.ObjectId, ref: 'User', default: [] }, ], chatId: { type: mongoose.Schema.Types.ObjectId, ref: 'Chat', required: true }, }); diff --git a/src/sockets/voiceCalls.ts b/src/sockets/voiceCalls.ts index 7ae47a7..5c2238c 100644 --- a/src/sockets/voiceCalls.ts +++ b/src/sockets/voiceCalls.ts @@ -1,4 +1,8 @@ +import Chat from '@base/models/chatModel'; +import VoiceCall from '@base/models/voiceCallModel'; +import mongoose from 'mongoose'; import { Server, Socket } from 'socket.io'; +import createVoiceCall from './voiceCallsServices'; interface CreateCallData { chatId: string; @@ -22,7 +26,22 @@ async function handleCreateCall( data: CreateCallData, userId: string ) { - console.log('Inside Create Call'); + const { targetId } = data; + let { chatId } = data; + + if (targetId && !chatId) { + //TODO: Create a new chat between the target and the user. + chatId = '123'; + } + + const voiceCall = await createVoiceCall(chatId, userId); + + io.to(chatId).emit('CALL-STARTED', { + snederId: userId, + chatId, + voiceCallId: voiceCall._id, + }); + //TODO: Don't forget to test create call } async function handleJoinCall( diff --git a/src/sockets/voiceCallsServices.ts b/src/sockets/voiceCallsServices.ts new file mode 100644 index 0000000..d378fbc --- /dev/null +++ b/src/sockets/voiceCallsServices.ts @@ -0,0 +1,17 @@ +import Chat from '@base/models/chatModel'; +import VoiceCall from '@base/models/voiceCallModel'; +import mongoose from 'mongoose'; + +export default async function createVoiceCall(chatId: string, userId: string) { + const chat = await Chat.findById(chatId); + + const voiceCall = new VoiceCall({ + callType: chat?.type === 'private' ? 'private' : 'group', + senderId: new mongoose.Types.ObjectId(userId), + chatId: new mongoose.Types.ObjectId(chatId), + }); + + await voiceCall.save(); + + return voiceCall; +} From d70a16dff35b81662f7e6150d8a64110158ecacd Mon Sep 17 00:00:00 2001 From: akramhany Date: Thu, 12 Dec 2024 16:11:19 +0200 Subject: [PATCH 05/54] feat(voice-calls): Implement making clients join calls and notifying others about it too --- src/sockets/voiceCalls.ts | 27 ++++++++++++------- src/sockets/voiceCallsServices.ts | 45 ++++++++++++++++++++++++++++++- 2 files changed, 61 insertions(+), 11 deletions(-) diff --git a/src/sockets/voiceCalls.ts b/src/sockets/voiceCalls.ts index 5c2238c..d206f19 100644 --- a/src/sockets/voiceCalls.ts +++ b/src/sockets/voiceCalls.ts @@ -1,8 +1,5 @@ -import Chat from '@base/models/chatModel'; -import VoiceCall from '@base/models/voiceCallModel'; -import mongoose from 'mongoose'; import { Server, Socket } from 'socket.io'; -import createVoiceCall from './voiceCallsServices'; +import { addClientToCall, createVoiceCall } from './voiceCallsServices'; interface CreateCallData { chatId: string; @@ -41,7 +38,6 @@ async function handleCreateCall( chatId, voiceCallId: voiceCall._id, }); - //TODO: Don't forget to test create call } async function handleJoinCall( @@ -50,7 +46,18 @@ async function handleJoinCall( data: JoinLeaveCallData, userId: string ) { - console.log('Inside Join Call'); + const { voiceCallId } = data; + console.log('userId type: ', typeof userId); + console.log('voiceCallId type: ', typeof voiceCallId); + + socket.join(voiceCallId); + + await addClientToCall(socket, userId, voiceCallId); + + socket.to(voiceCallId).emit('CLIENT-JOINED', { + clientId: userId, + voiceCallId, + }); } async function handleSignal( @@ -77,19 +84,19 @@ async function registerVoiceCallHandlers( userId: string ) { socket.on('CREATE-CALL', (data: CreateCallData) => { - handleCreateCall(io, socket, data, userId); + handleCreateCall(io, socket, data, userId.toString()); }); socket.on('JOIN-CALL', (data: JoinLeaveCallData) => { - handleJoinCall(io, socket, data, userId); + handleJoinCall(io, socket, data, userId.toString()); }); socket.on('SIGNAL', (data: SignalData) => { - handleSignal(io, socket, data, userId); + handleSignal(io, socket, data, userId.toString()); }); socket.on('LEAVE', (data: JoinLeaveCallData) => { - handleLeaveCall(io, socket, data, userId); + handleLeaveCall(io, socket, data, userId.toString()); }); } diff --git a/src/sockets/voiceCallsServices.ts b/src/sockets/voiceCallsServices.ts index d378fbc..d3eeb67 100644 --- a/src/sockets/voiceCallsServices.ts +++ b/src/sockets/voiceCallsServices.ts @@ -1,8 +1,18 @@ import Chat from '@base/models/chatModel'; import VoiceCall from '@base/models/voiceCallModel'; +import IVoiceCall from '@base/types/voiceCall'; import mongoose from 'mongoose'; +import { Socket } from 'socket.io'; -export default async function createVoiceCall(chatId: string, userId: string) { +interface ClientSocketMap { + [voiceCallId: string]: { + [userId: string]: string; + }; +} + +const clientSocketMap: ClientSocketMap = {}; + +export async function createVoiceCall(chatId: string, userId: string) { const chat = await Chat.findById(chatId); const voiceCall = new VoiceCall({ @@ -15,3 +25,36 @@ export default async function createVoiceCall(chatId: string, userId: string) { return voiceCall; } + +export async function addClientToCall( + socket: Socket, + userId: string, + voiceCallId: string +) { + // Add the client socket id into the map + if (!clientSocketMap[voiceCallId]) clientSocketMap[voiceCallId] = {}; + clientSocketMap[voiceCallId][userId] = socket.id; + + // Add a user Id object into the call current participants. + const voiceCall: IVoiceCall = await VoiceCall.findById(voiceCallId); + const userIdObj = new mongoose.Types.ObjectId(userId); + + const userIdIndex = voiceCall.currentParticipants.indexOf(userIdObj); + + if (userIdIndex === -1) { + voiceCall.currentParticipants.push(userIdObj); + } else { + voiceCall.currentParticipants[userIdIndex] = userIdObj; + } + + await voiceCall.save(); + console.log( + 'voice call currentParticipants: ', + voiceCall.currentParticipants + ); + console.log('map data: ', clientSocketMap); +} + +export function getClientSocketMap(): ClientSocketMap { + return clientSocketMap; +} From 38cec50ac16f0142e6916274c0ba639827d05774 Mon Sep 17 00:00:00 2001 From: akramhany Date: Fri, 13 Dec 2024 10:27:38 +0200 Subject: [PATCH 06/54] feat(voice-calls): Implement Signal events to send data between clients --- src/sockets/voiceCalls.ts | 23 +++++++++++++++++------ src/sockets/voiceCallsServices.ts | 16 +++++++++++----- 2 files changed, 28 insertions(+), 11 deletions(-) diff --git a/src/sockets/voiceCalls.ts b/src/sockets/voiceCalls.ts index d206f19..76c8e5c 100644 --- a/src/sockets/voiceCalls.ts +++ b/src/sockets/voiceCalls.ts @@ -1,5 +1,9 @@ import { Server, Socket } from 'socket.io'; -import { addClientToCall, createVoiceCall } from './voiceCallsServices'; +import { + addClientToCall, + createVoiceCall, + getClientSocketId, +} from './voiceCallsServices'; interface CreateCallData { chatId: string; @@ -47,8 +51,6 @@ async function handleJoinCall( userId: string ) { const { voiceCallId } = data; - console.log('userId type: ', typeof userId); - console.log('voiceCallId type: ', typeof voiceCallId); socket.join(voiceCallId); @@ -63,10 +65,19 @@ async function handleJoinCall( async function handleSignal( io: Server, socket: Socket, - data: SignalData, + signalData: SignalData, userId: string ) { - console.log('Inside Signal'); + const { type, targetId, voiceCallId, data } = signalData; + + const socketId = getClientSocketId(voiceCallId, targetId); + + io.to(socketId).emit('SIGNAL-CLIENT', { + type, + senderId: userId, + voiceCallId, + data, + }); } async function handleLeaveCall( @@ -91,7 +102,7 @@ async function registerVoiceCallHandlers( handleJoinCall(io, socket, data, userId.toString()); }); - socket.on('SIGNAL', (data: SignalData) => { + socket.on('SIGNAL-SERVER', (data: SignalData) => { handleSignal(io, socket, data, userId.toString()); }); diff --git a/src/sockets/voiceCallsServices.ts b/src/sockets/voiceCallsServices.ts index d3eeb67..c4b3f86 100644 --- a/src/sockets/voiceCallsServices.ts +++ b/src/sockets/voiceCallsServices.ts @@ -1,3 +1,4 @@ +import AppError from '@base/errors/AppError'; import Chat from '@base/models/chatModel'; import VoiceCall from '@base/models/voiceCallModel'; import IVoiceCall from '@base/types/voiceCall'; @@ -48,13 +49,18 @@ export async function addClientToCall( } await voiceCall.save(); - console.log( - 'voice call currentParticipants: ', - voiceCall.currentParticipants - ); - console.log('map data: ', clientSocketMap); } export function getClientSocketMap(): ClientSocketMap { return clientSocketMap; } + +export function getClientSocketId(voiceCallId: string, userId: string) { + if (!clientSocketMap[voiceCallId]) + throw new Error('No voice call exists with this id!'); + + if (!clientSocketMap[voiceCallId][userId]) + throw new Error('No socket exists for this user id!'); + + return clientSocketMap[voiceCallId][userId]; +} From 478bf4bfc912c8c60cd00d91e82b1891444f0b2b Mon Sep 17 00:00:00 2001 From: akramhany Date: Fri, 13 Dec 2024 11:23:17 +0200 Subject: [PATCH 07/54] feat(voice-calls): Implement leaving a voice call --- src/sockets/voiceCalls.ts | 14 ++++++++++- src/sockets/voiceCallsServices.ts | 40 ++++++++++++++++++++++++++++++- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/src/sockets/voiceCalls.ts b/src/sockets/voiceCalls.ts index 76c8e5c..8d128b3 100644 --- a/src/sockets/voiceCalls.ts +++ b/src/sockets/voiceCalls.ts @@ -3,6 +3,7 @@ import { addClientToCall, createVoiceCall, getClientSocketId, + removeClientFromCall, } from './voiceCallsServices'; interface CreateCallData { @@ -86,7 +87,16 @@ async function handleLeaveCall( data: JoinLeaveCallData, userId: string ) { - console.log('Inside Leave Call'); + const { voiceCallId } = data; + + socket.leave(voiceCallId); + + await removeClientFromCall(userId, voiceCallId); + + socket.to(voiceCallId).emit('CLIENT-LEFT', { + clientId: userId, + voiceCallId, + }); } async function registerVoiceCallHandlers( @@ -109,6 +119,8 @@ async function registerVoiceCallHandlers( socket.on('LEAVE', (data: JoinLeaveCallData) => { handleLeaveCall(io, socket, data, userId.toString()); }); + + //TODO: DON'T FORGET TO HANDLE ERRORS (WRAP HANDLERS WITH ANOTHER FUNCTION LIKE catchAsync) } export default registerVoiceCallHandlers; diff --git a/src/sockets/voiceCallsServices.ts b/src/sockets/voiceCallsServices.ts index c4b3f86..7479159 100644 --- a/src/sockets/voiceCallsServices.ts +++ b/src/sockets/voiceCallsServices.ts @@ -1,4 +1,3 @@ -import AppError from '@base/errors/AppError'; import Chat from '@base/models/chatModel'; import VoiceCall from '@base/models/voiceCallModel'; import IVoiceCall from '@base/types/voiceCall'; @@ -13,6 +12,22 @@ interface ClientSocketMap { const clientSocketMap: ClientSocketMap = {}; +async function endVoiceCall(voiceCallId: string) { + // Delete voice call entry in map. + delete clientSocketMap[voiceCallId]; + + const voiceCall: IVoiceCall = await VoiceCall.findById(voiceCallId); + + // Calculate duration in minutes + voiceCall.duration = Math.floor( + (Date.now() - voiceCall.timestamp.getTime()) / (1000 * 60) + ); + + voiceCall.status = 'finished'; + + await voiceCall.save(); +} + export async function createVoiceCall(chatId: string, userId: string) { const chat = await Chat.findById(chatId); @@ -51,6 +66,29 @@ export async function addClientToCall( await voiceCall.save(); } +export async function removeClientFromCall( + userId: string, + voiceCallId: string +) { + // Delete the userId entry from the map + if (clientSocketMap[voiceCallId]) delete clientSocketMap[voiceCallId][userId]; + + // Delete the userId from current participants of voice call. + const voiceCall: IVoiceCall = await VoiceCall.findById(voiceCallId); + const userIdObj = new mongoose.Types.ObjectId(userId); + + const userIdIndex = voiceCall.currentParticipants.indexOf(userIdObj); + + if (userIdIndex !== -1) { + voiceCall.currentParticipants.splice(userIdIndex, 1); + await voiceCall.save(); + } + + if (voiceCall.currentParticipants.length === 0) { + await endVoiceCall(voiceCallId); + } +} + export function getClientSocketMap(): ClientSocketMap { return clientSocketMap; } From d06813d49f41e4a4f21d60c0b5c11f284843d6a1 Mon Sep 17 00:00:00 2001 From: akramhany Date: Fri, 13 Dec 2024 11:29:53 +0200 Subject: [PATCH 08/54] feat(voice-calls): Comment lines that throws error until implementing error handling --- src/sockets/voiceCalls.ts | 4 ++-- src/sockets/voiceCallsServices.ts | 19 ++++++++++++++----- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/src/sockets/voiceCalls.ts b/src/sockets/voiceCalls.ts index 8d128b3..f4598ad 100644 --- a/src/sockets/voiceCalls.ts +++ b/src/sockets/voiceCalls.ts @@ -53,10 +53,10 @@ async function handleJoinCall( ) { const { voiceCallId } = data; - socket.join(voiceCallId); - await addClientToCall(socket, userId, voiceCallId); + socket.join(voiceCallId); + socket.to(voiceCallId).emit('CLIENT-JOINED', { clientId: userId, voiceCallId, diff --git a/src/sockets/voiceCallsServices.ts b/src/sockets/voiceCallsServices.ts index 7479159..c031da8 100644 --- a/src/sockets/voiceCallsServices.ts +++ b/src/sockets/voiceCallsServices.ts @@ -47,14 +47,17 @@ export async function addClientToCall( userId: string, voiceCallId: string ) { - // Add the client socket id into the map - if (!clientSocketMap[voiceCallId]) clientSocketMap[voiceCallId] = {}; - clientSocketMap[voiceCallId][userId] = socket.id; - // Add a user Id object into the call current participants. const voiceCall: IVoiceCall = await VoiceCall.findById(voiceCallId); const userIdObj = new mongoose.Types.ObjectId(userId); + //TODO: UNCOMMENT AFTER IMPLEMENTING ERROR HANDLING + /* + if (voiceCall.status === 'finished') { + throw new Error('This voice call has already finished!'); + } + */ + const userIdIndex = voiceCall.currentParticipants.indexOf(userIdObj); if (userIdIndex === -1) { @@ -64,6 +67,10 @@ export async function addClientToCall( } await voiceCall.save(); + + // Add the client socket id into the map + if (!clientSocketMap[voiceCallId]) clientSocketMap[voiceCallId] = {}; + clientSocketMap[voiceCallId][userId] = socket.id; } export async function removeClientFromCall( @@ -94,11 +101,13 @@ export function getClientSocketMap(): ClientSocketMap { } export function getClientSocketId(voiceCallId: string, userId: string) { + //TODO: UNCOMMENT AFTER IMPLEMENTING ERROR HANDLING + /* if (!clientSocketMap[voiceCallId]) throw new Error('No voice call exists with this id!'); if (!clientSocketMap[voiceCallId][userId]) throw new Error('No socket exists for this user id!'); - + */ return clientSocketMap[voiceCallId][userId]; } From 0725a99938c9c906b6e283cfcff55cb6080f2cea Mon Sep 17 00:00:00 2001 From: akramhany Date: Fri, 13 Dec 2024 12:59:37 +0200 Subject: [PATCH 09/54] feat(voice-calls): Implement an endpoint to get all voice calls in a chat --- src/controllers/chatController.ts | 17 +++++++++++++++++ src/routes/chatRoute.ts | 9 ++++++++- 2 files changed, 25 insertions(+), 1 deletion(-) diff --git a/src/controllers/chatController.ts b/src/controllers/chatController.ts index ab0630f..3a32cb6 100644 --- a/src/controllers/chatController.ts +++ b/src/controllers/chatController.ts @@ -16,6 +16,7 @@ import mongoose from 'mongoose'; import GroupChannel from '@base/models/groupChannelModel'; import crypto from 'crypto'; import Invite from '@base/models/invite'; +import VoiceCall from '@base/models/voiceCallModel'; export const getAllChats = catchAsync( async (req: Request, res: Response, next: NextFunction) => { @@ -293,3 +294,19 @@ export const join = catchAsync( }); } ); + +export const getVoiceCallsInChat = catchAsync( + async (req: Request, res: Response, next: NextFunction) => { + const { chatId } = req.params; + + const voiceCalls = await VoiceCall.find({ chatId }); + + res.status(200).json({ + status: 'success', + message: 'voice calls retrieved successfuly', + data: { + voiceCalls, + }, + }); + } +); diff --git a/src/routes/chatRoute.ts b/src/routes/chatRoute.ts index a3bbf09..fd7b8f7 100644 --- a/src/routes/chatRoute.ts +++ b/src/routes/chatRoute.ts @@ -13,6 +13,7 @@ import { updateChatPicture, invite, join, + getVoiceCallsInChat, } from '@base/controllers/chatController'; import { protect } from '@base/middlewares/authMiddleware'; import upload from '@base/config/fileUploads'; @@ -23,7 +24,12 @@ const router = Router(); router.use(protect); router.get('/', getAllChats); router.post('/media', upload.single('file'), postMediaFile); -router.patch('/picture/:chatId', restrictTo(), upload.single('file'), updateChatPicture); +router.patch( + '/picture/:chatId', + restrictTo(), + upload.single('file'), + updateChatPicture +); router.patch('/privacy/:chatId', restrictTo('admin'), setPrivacy); router.patch('/destruct/:chatId', restrictTo(), enableSelfDestructing); @@ -34,6 +40,7 @@ router.patch('/unmute/:chatId', restrictTo(), unmuteChat); router.get('/invite/:chatId', restrictTo('admin'), invite); router.post('/join/:token', join); +router.get('/voice-calls/:chatId', getVoiceCallsInChat); router.get('/messages/:chatId', restrictTo(), getMessages); router.get('/members/:chatId', restrictTo(), getChatMembers); router.get('/:chatId', restrictTo(), getChat); From 95bba44e9a1900c243dfc340ec076be8f4dda353 Mon Sep 17 00:00:00 2001 From: akramhany Date: Fri, 13 Dec 2024 14:51:00 +0200 Subject: [PATCH 10/54] chore(swagger): Document all calls-related sockets and endpoints --- docs/api/chat.swagger.ts | 64 +++++++++++++++++++++++++++ docs/api/sockets.swagger.ts | 86 +++++++++++++++++++++++++++++++++++++ src/routes/chatRoute.ts | 2 +- 3 files changed, 151 insertions(+), 1 deletion(-) diff --git a/docs/api/chat.swagger.ts b/docs/api/chat.swagger.ts index 06cbb38..daa0f28 100644 --- a/docs/api/chat.swagger.ts +++ b/docs/api/chat.swagger.ts @@ -1063,3 +1063,67 @@ * type: string * example: login first */ + +/** + * @swagger + * /chats/voice-calls/{chatId}: + * get: + * summary: Retrieve voice calls in a specific chat. + * description: Fetch all voice calls associated with a given chat ID. Access is restricted to chat members. + * tags: + * - Chat + * parameters: + * - in: path + * name: chatId + * required: true + * schema: + * type: string + * description: The ID of the chat whose voice calls are being retrieved. + * responses: + * 200: + * description: Voice calls retrieved successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: success + * message: + * type: string + * example: voice calls retrieved successfully + * data: + * type: object + * properties: + * voiceCalls: + * type: array + * items: + * type: object + * 400: + * description: Missing required fields or invalid request. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: fail + * message: + * type: string + * example: missing required fields + * 403: + * description: Unauthorized request due to missing or invalid user authentication. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: fail + * message: + * type: string + * example: login first + */ diff --git a/docs/api/sockets.swagger.ts b/docs/api/sockets.swagger.ts index af4a5e4..3772dea 100644 --- a/docs/api/sockets.swagger.ts +++ b/docs/api/sockets.swagger.ts @@ -912,3 +912,89 @@ * type: string * example: This chat does not exist or has been deleted */ + +/** + * @swagger + * CREATE-CALL: + * post: + * summary: Initiates a new voice call. + * tags: [Sockets] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * chatId: + * type: string + * description: The unique ID of the chat where the voice call is started. + * targetId: + * type: string + * description: Used to when there is no chat between the two users. + * example: "98765" + */ + +/** + * @swagger + * JOIN-CALL: + * post: + * summary: Joins an existing voice call. + * tags: [Sockets] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * voiceCallId: + * type: string + * description: The ID of the voice call to join. + */ + +/** + * @swagger + * SIGNAL-SERVER: + * post: + * summary: Sends signaling data to a specific client. + * tags: [Sockets] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * type: + * type: string + * enum: [ICE, OFFER, ANSWER] + * description: The type of signaling message. + * targetId: + * type: string + * description: The ID of the target client. + * voiceCallId: + * type: string + * description: The ID of the voice call. + * data: + * type: object + * description: The actual signaling data. + */ + +/** + * @swagger + * LEAVE: + * post: + * summary: Leaves a voice call. + * tags: [Sockets] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * voiceCallId: + * type: string + * description: The ID of the voice call to leave. + */ diff --git a/src/routes/chatRoute.ts b/src/routes/chatRoute.ts index fd7b8f7..3743531 100644 --- a/src/routes/chatRoute.ts +++ b/src/routes/chatRoute.ts @@ -40,7 +40,7 @@ router.patch('/unmute/:chatId', restrictTo(), unmuteChat); router.get('/invite/:chatId', restrictTo('admin'), invite); router.post('/join/:token', join); -router.get('/voice-calls/:chatId', getVoiceCallsInChat); +router.get('/voice-calls/:chatId', restrictTo(), getVoiceCallsInChat); router.get('/messages/:chatId', restrictTo(), getMessages); router.get('/members/:chatId', restrictTo(), getChatMembers); router.get('/:chatId', restrictTo(), getChat); From ded5efa1a5e4ab090f8da3df0c0a382bc6a24854 Mon Sep 17 00:00:00 2001 From: Peter Safwat <118059454+PeterSafwatHBK@users.noreply.github.com> Date: Fri, 13 Dec 2024 18:37:40 +0200 Subject: [PATCH 11/54] Update searchController.ts --- src/controllers/searchController.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/src/controllers/searchController.ts b/src/controllers/searchController.ts index c33d5ea..736baac 100644 --- a/src/controllers/searchController.ts +++ b/src/controllers/searchController.ts @@ -6,7 +6,7 @@ import catchAsync from '@utils/catchAsync'; export const searchMessages = catchAsync(async (req: any, res: Response, next: NextFunction) => { try { - const { query, searchSpace, filter, isGlobalSearch } = req.query; + const { query, searchSpace, filter, isGlobalSearch } = req.body; if (!query || !searchSpace || typeof isGlobalSearch === 'undefined') { return res.status(400).json({ message: 'Query, searchSpace, and isGlobalSearch are required' }); @@ -22,15 +22,21 @@ export const searchMessages = catchAsync(async (req: any, res: Response, next: N } const spaces = (searchSpace as string).split(','); + const messageTypes: string[] = []; + if (spaces.includes('chats')) searchConditions.chatId = { $exists: true }; - if (spaces.includes('channels')) searchConditions.messageType = 'channel'; - if (spaces.includes('groups')) searchConditions.messageType = 'group'; - + if (spaces.includes('channels')) messageTypes.push('channel'); + if (spaces.includes('groups')) messageTypes.push('group'); + + if (messageTypes.length > 0) { + searchConditions.messageType = { $in: messageTypes }; + } + const userChats = await Chat.find({ members: { $elemMatch: { _id: req.user._id } }, }).select('_id'); - - console.log(userChats) + + console.log(userChats); const chatIds = userChats.map((chat) => chat._id); searchConditions.chatId = { $in: chatIds }; From 3da494f41a7a9951251e5d4c593e9fcc6a1df5cc Mon Sep 17 00:00:00 2001 From: AhmedHamed3699 Date: Sun, 15 Dec 2024 19:39:36 +0200 Subject: [PATCH 12/54] fix(messages): solve pagination bug --- src/controllers/chatController.ts | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/controllers/chatController.ts b/src/controllers/chatController.ts index 3a32cb6..c7afa91 100644 --- a/src/controllers/chatController.ts +++ b/src/controllers/chatController.ts @@ -57,15 +57,19 @@ export const getMessages = catchAsync( async (req: Request, res: Response, next: NextFunction) => { const { chatId } = req.params; - const page: number = parseInt(req.query.page as string, 10) || 1; + const pageByMsgId = req.query.page === '0' ? undefined : req.query.page; const limit: number = parseInt(req.query.limit as string, 10) || 20; - const skip: number = (page - 1) * limit; - const messages = await Message.find({ chatId }).limit(limit).skip(skip); + const filter: any = { chatId }; + if (pageByMsgId) { + filter._id = { $lt: pageByMsgId }; + } + console.log(filter); + const messages = await Message.find(filter).sort({ _id: -1 }).limit(limit); res.status(200).json({ status: 'success', message: 'messages retreived successfuly', - data: { messages, nextPage: page + 1 }, + data: { messages, nextPage: messages[messages.length - 1]._id }, }); } ); From f53cfcdb8e8fad0cab0e4ff6f8d36dce27737a0e Mon Sep 17 00:00:00 2001 From: AhmedHamed3699 Date: Mon, 16 Dec 2024 00:55:29 +0200 Subject: [PATCH 13/54] fix(messages): messages sorting bug --- src/controllers/chatController.ts | 13 ++++++++++--- src/models/communicationModel.ts | 2 ++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/controllers/chatController.ts b/src/controllers/chatController.ts index c7afa91..25504ce 100644 --- a/src/controllers/chatController.ts +++ b/src/controllers/chatController.ts @@ -63,13 +63,20 @@ export const getMessages = catchAsync( if (pageByMsgId) { filter._id = { $lt: pageByMsgId }; } - console.log(filter); - const messages = await Message.find(filter).sort({ _id: -1 }).limit(limit); + const messages = await Message.find(filter) + .limit(limit) + .sort({ timestamp: 1 }); + + if (!messages || messages.length === 0) { + return next(new AppError('No messages found', 404)); + } + const nextPage = + messages.length < limit ? undefined : messages[messages.length - 1]._id; res.status(200).json({ status: 'success', message: 'messages retreived successfuly', - data: { messages, nextPage: messages[messages.length - 1]._id }, + data: { messages, nextPage }, }); } ); diff --git a/src/models/communicationModel.ts b/src/models/communicationModel.ts index 4290cf4..42bf1e0 100644 --- a/src/models/communicationModel.ts +++ b/src/models/communicationModel.ts @@ -33,5 +33,7 @@ const communicationSchema = new mongoose.Schema( } ); +communicationSchema.index({ timestamp: 1 }, { unique: true, background: true }); + const Communication = mongoose.model('Communication', communicationSchema); export default Communication; From ea4d1de91274a62c3498e876223cc097d491da02 Mon Sep 17 00:00:00 2001 From: AhmedHamed3699 Date: Mon, 16 Dec 2024 17:00:20 +0200 Subject: [PATCH 14/54] fix: solved some bugs and refactored a bit --- .env.example | 5 + docs/api/sockets.swagger.ts | 83 ++- src/app.ts | 6 +- src/config/allowedOrigins.json | 1 + src/controllers/chatController.ts | 6 +- src/models/{invite.ts => inviteModel.ts} | 0 src/sockets/MessagingServices.ts | 91 +++ src/sockets/chat.ts | 506 ++++++++++++++++- src/sockets/messages.ts | 222 +++++++- src/sockets/services.ts | 694 ----------------------- src/sockets/socket.ts | 62 +- src/utils/email.ts | 10 +- 12 files changed, 875 insertions(+), 811 deletions(-) rename src/models/{invite.ts => inviteModel.ts} (100%) create mode 100644 src/sockets/MessagingServices.ts delete mode 100644 src/sockets/services.ts diff --git a/.env.example b/.env.example index c0f190d..21d55e3 100644 --- a/.env.example +++ b/.env.example @@ -8,6 +8,9 @@ MONGO_DB_PASSWORD=1234 REDIS_DOCKER_URL=redis://redis:6379 REDIS_LOCALHOST_URL=redis://localhost:6379 +SERVER_URL=http://localhost:3000 +WEBSOCKET_URL=ws://localhost:3000 + SESSION_SECRET=session-secret SESSION_EXPIRES_IN=180d @@ -15,6 +18,8 @@ RECAPTCHA_SECRET=recaptcha-secret RECAPTCHA_SITE=recaptcha-site DISABLE_RECAPTCHA=false +EMAIL_PROVIDER=mailtrap-gmail + TELWARE_EMAIL=telware@gmail.com TELWARE_PASSWORD=telware-password GMAIL_HOST=smtp.gmail.com diff --git a/docs/api/sockets.swagger.ts b/docs/api/sockets.swagger.ts index 3772dea..466f9e1 100644 --- a/docs/api/sockets.swagger.ts +++ b/docs/api/sockets.swagger.ts @@ -7,7 +7,7 @@ /** * @swagger - * /SEND-FORWARD-REPLY: + * SEND_MESSAGE: * post: * summary: Handle messaging functionality, including creating chats, replying, and forwarding messages. * tags: @@ -124,7 +124,7 @@ /** * @swagger - * /EDIT_MESSAGE: + * EDIT_MESSAGE_CLIENT: * patch: * summary: Edits an existing message in the chat. * description: Edits an existing message in the chat. It is not possible to edit forwarded messages. @@ -249,7 +249,7 @@ /** * @swagger - * /DELETE_MESSAGE: + * DELETE_MESSAGE_CLIENT: * delete: * summary: Deletes a Message. * description: Deletes a message from the chat based on the provided message ID. If the message is found, it is deleted from the chat. @@ -324,7 +324,7 @@ /** * @swagger - * /UPDATE_DRAFT_CLIENT: + * UPDATE_DRAFT_CLIENT: * post: * summary: Updates a draft message * description: Updates an existing draft message with new content. The updated draft is saved and the client is notified of the update. @@ -379,30 +379,28 @@ /** * @swagger - * /UPDATE_DRAFT_SERVER: + * CREATE_PRIVATE_CHAT: * post: - * summary: Emits an event to update a draft message on the server - * description: Emits an event to update an existing draft message with new content on the server. The server processes the update and notifies the client of the status. - * tags: [Sockets] + * summary: "Create a new private chat" + * description: "This socket event allows a user to create a new private chat. It validates the user's existence and establishes a one-to-one chat." + * tags: + * - Sockets + * operationId: "createPrivateChat" * requestBody: * required: true * content: * application/json: * schema: * type: object - * required: - * - chatId - * - draft * properties: - * chatId: + * memberId: * type: string - * description: The unique ID of the chat whose draft is being updated. - * draft: - * type: string - * description: The new content of the draft message. + * description: "ID of the user to be added as the other member of the private chat." + * required: + * - memberId * responses: * 200: - * description: Draft update event emitted successfully. + * description: "Private chat created successfully." * content: * application/json: * schema: @@ -410,28 +408,29 @@ * properties: * success: * type: boolean - * description: Indicates if the event was successfully emitted. + * example: true * message: * type: string - * description: A message describing the result. + * example: "Chat created successfully." + * data: + * type: object + * properties: + * _id: + * type: string + * description: "ID of the newly created private chat." + * members: + * type: array + * items: + * type: object + * properties: + * user: + * type: string + * description: "ID of the user." + * Role: + * type: string + * description: "Role of the user in the private chat (e.g., 'member')." * 400: - * description: Missing required fields or invalid input. - * content: - * application/json: - * schema: - * type: object - * properties: - * success: - * type: boolean - * description: Indicates if the operation was successful. - * message: - * type: string - * description: An error message describing the problem. - * error: - * type: string - * description: Details about the error (e.g., invalid input). - * 500: - * description: Internal server error occurred while emitting the event. + * description: "Invalid input or constraints violated." * content: * application/json: * schema: @@ -439,18 +438,18 @@ * properties: * success: * type: boolean - * description: Indicates if the operation was successful. + * example: false * message: * type: string - * description: An error message describing the result. + * example: "Failed to create the chat." * error: * type: string - * description: Details about the server error. + * description: "Error message describing the issue." */ /** * @swagger - * /CREATE_GROUP_CHANNEL: + * CREATE_GROUP_CHANNEL: * post: * summary: "Create a new group channel" * description: "This socket event allows a user to create a new group channel. It validates the user's login status, group size, and adds members with roles." @@ -537,7 +536,7 @@ /** * @swagger - * /DELETE_GROUP_CHANNEL_CLIENT: + * DELETE_GROUP_CHANNEL_CLIENT: * delete: * summary: "Delete a group or channel" * description: "This socket event allows the admin of a group channel to delete the group. All members will be informed about the deletion." @@ -593,7 +592,7 @@ /** * @swagger - * /LEAVE_GROUP_CHANNEL_CLIENT: + * LEAVE_GROUP_CHANNEL_CLIENT: * delete: * summary: "Leave a group channel" * description: "Allows a user to leave a group channel. Other members in the group will be notified of the member's departure." diff --git a/src/app.ts b/src/app.ts index f486714..016d5d8 100644 --- a/src/app.ts +++ b/src/app.ts @@ -36,11 +36,11 @@ const swaggerOptions = { }, servers: [ { - url: 'http://localhost:3000/api/v1/', - description: 'Local server', + url: `${process.env.SERVER_URL}/api/v1/`, + description: 'HTTP server', }, { - url: 'ws://localhost:3000', + url: process.env.WEBSOCKET_URL, description: 'WebSocket server', }, ], diff --git a/src/config/allowedOrigins.json b/src/config/allowedOrigins.json index d712796..4247c98 100644 --- a/src/config/allowedOrigins.json +++ b/src/config/allowedOrigins.json @@ -3,6 +3,7 @@ "http://localhost:5174", "http://127.0.0.1:5174", "http://telware.tech", + "http://dev.telware.tech", "http://testing.telware.tech", "http://api.telware.tech", "http://api.testing.telware.tech", diff --git a/src/controllers/chatController.ts b/src/controllers/chatController.ts index 25504ce..6a82d5c 100644 --- a/src/controllers/chatController.ts +++ b/src/controllers/chatController.ts @@ -15,7 +15,7 @@ import { NextFunction, Request, Response } from 'express'; import mongoose from 'mongoose'; import GroupChannel from '@base/models/groupChannelModel'; import crypto from 'crypto'; -import Invite from '@base/models/invite'; +import Invite from '@base/models/inviteModel'; import VoiceCall from '@base/models/voiceCallModel'; export const getAllChats = catchAsync( @@ -63,6 +63,10 @@ export const getMessages = catchAsync( if (pageByMsgId) { filter._id = { $lt: pageByMsgId }; } + if (req.query.timestamp) { + filter.timestamp = { $gte: req.query.timestamp }; + } + const messages = await Message.find(filter) .limit(limit) .sort({ timestamp: 1 }); diff --git a/src/models/invite.ts b/src/models/inviteModel.ts similarity index 100% rename from src/models/invite.ts rename to src/models/inviteModel.ts diff --git a/src/sockets/MessagingServices.ts b/src/sockets/MessagingServices.ts new file mode 100644 index 0000000..5caee2c --- /dev/null +++ b/src/sockets/MessagingServices.ts @@ -0,0 +1,91 @@ +import { Socket } from 'socket.io'; +import { getChatIds } from '@services/chatService'; +import { Types } from 'mongoose'; +import { getSocketsByUserId } from '@base/services/sessionService'; +import User from '@base/models/userModel'; +import Chat from '@base/models/chatModel'; + +export interface Member { + _id: Types.ObjectId; + Role: 'member' | 'admin'; +} + +export const check = async (chatId: any, ack: Function, senderId: any) => { + if (!chatId) { + return ack({ success: false, message: 'provide the chatId' }); + } + const chat = await Chat.findById(chatId); + if (!chat) { + return ack({ + success: false, + message: 'no chat found with the provided id', + }); + } + + if (chat.type === 'private') + return ack({ + success: false, + message: 'this is a private chat!', + }); + const chatMembers = chat.members; + const chatMembersIds = chatMembers.map((m: any) => m._id); + if (chatMembersIds.length === 0) + return ack({ + success: false, + message: 'this chat is deleted and it no longer exists', + }); + + const admin: Member = chatMembers.find((m) => + m.user.equals(senderId) + ) as unknown as Member; + + if (!admin || admin.Role === 'member') + return ack({ + success: false, + message: 'you do not have permission', + }); +}; + +export const inform = async (io: any, userId: string, data: any, event: string) => { + let memberSocket; + const socketIds = await getSocketsByUserId(userId); + if (!socketIds || socketIds.length !== 0) + socketIds.forEach((socketId: any) => { + memberSocket = io.sockets.sockets.get(socketId); + if (memberSocket) memberSocket.emit(event, data); + }); +}; + +export const joinRoom = async (io: any, roomId: String, userId: Types.ObjectId) => { + const socketIds = await getSocketsByUserId(userId); + socketIds.forEach((socketId: string) => { + const socket = io.sockets.sockets.get(socketId); + if (socket) socket.join(roomId); + }); +}; + +export const updateDraft = async ( + io: any, + senderId: string, + chatId: string, + content: string +) => { + await User.findByIdAndUpdate( + senderId, + { $set: { 'chats.$[chat].draft': content } }, + { + arrayFilters: [{ 'chat.chat': chatId }], + } + ); + await inform(io, senderId, { chatId, draft: content }, 'UPDATE_DRAFT_SERVER'); +}; + +export const joinAllRooms = async ( + socket: Socket, + userId: Types.ObjectId +) => { + const chatIds = await getChatIds(userId); + chatIds.forEach((chatId: Types.ObjectId) => { + socket.join(chatId.toString()); + }); +}; diff --git a/src/sockets/chat.ts b/src/sockets/chat.ts index 38df82c..cfafaed 100644 --- a/src/sockets/chat.ts +++ b/src/sockets/chat.ts @@ -1,7 +1,477 @@ import { Server, Socket } from 'socket.io'; -import { updateDraft } from './services'; +import { ObjectId } from 'mongodb'; +import User from '@models/userModel'; +import Chat from '@models/chatModel'; +import GroupChannel from '@models/groupChannelModel'; +import NormalChat from '@base/models/normalChatModel'; +import { + check, + inform, + joinRoom, + Member, + updateDraft, +} from './MessagingServices'; -export const handleDraftMessage = async ( +const handleAddAdmins = async ( + io: any, + data: any, + ack: Function, + senderId: any +) => { + const { members, chatId } = data; + const forbiddenUsers: string[] = []; + const func = await check(chatId, ack, senderId); + const chat = await Chat.findById(chatId); + + if (func) return func; + + if (!chat || chat.isDeleted) + return ack({ + success: false, + message: 'Could not add admins to the group', + error: 'Chat not found', + }); + + await Promise.all( + members.map(async (memId: string) => { + const user = await User.findById(memId); + + if (!user) { + forbiddenUsers.push(memId); + return; + } + + const isMemberOfChat = chat.members.some((m) => m.user.equals(memId)); + + if (!isMemberOfChat) { + forbiddenUsers.push(memId); + return; + } + + await Chat.findByIdAndUpdate( + chatId, + { $set: { 'members.$[elem].Role': 'admin' } }, + { + new: true, + arrayFilters: [{ 'elem.user': memId }], + } + ); + + await inform(io, memId, { chatId }, 'ADD_ADMINS_SERVER'); + }) + ); + + if (forbiddenUsers.length > 0) { + return ack({ + success: false, + message: 'Some users could not be added as admins', + error: `Could not add users with IDs: ${forbiddenUsers.join(', ')}`, + }); + } + + ack({ + success: true, + message: 'Added admins successfully', + data: {}, + }); +}; + +const handleAddMembers = async ( + io: any, + data: any, + ack: Function, + senderId: any +) => { + const { chatId, users } = data; + const forbiddenUsers: string[] = []; + + const func = await check(chatId, ack, senderId); + if (func) return func; + + const chat = await Chat.findById(chatId); + if (!chat || chat.isDeleted) + return ack({ + success: false, + message: 'No chat found with the provided ID', + }); + + await Promise.all( + users.map(async (userId: any) => { + const user = await User.findById(userId); + + if (!user) { + forbiddenUsers.push(userId); + return; + } + + const isAlreadyMember = chat.members.some((m: any) => + m.user.equals(userId) + ); + if (!isAlreadyMember) chat.members.push({ user: userId, Role: 'member' }); + const userWasMember = user.chats.some((c: any) => c.chat.equals(chatId)); + if (!userWasMember) user.chats.push(chatId); + console.log(user.chats); + if (isAlreadyMember) { + forbiddenUsers.push(userId); + return; + } + + await inform(io, userId, { chatId }, 'ADD_MEMBERS_SERVER'); + }) + ); + + if (forbiddenUsers.length > 0) { + return ack({ + success: false, + message: 'Some users could not be added', + error: `Could not add users with IDs: ${forbiddenUsers.join(', ')}`, + }); + } + + await chat.save(); + + ack({ + success: true, + message: 'Members added successfully', + data: {}, + }); +}; + +const handleCreatePrivateChat = async ( + io: any, + socket: Socket, + data: any, + ack: Function, + senderId: any +) => { + const { memberId } = data; + const user = User.findById(senderId); + if (!user) + return ack({ + success: false, + message: 'Faild to create the chat', + error: 'you do not exsist!', + }); + + const newChat = new NormalChat({ + members: [ + { + user: memberId, + Role: 'member', + }, + { + user: senderId, + Role: 'member', + }, + ], + }); + + await newChat.save(); + await Promise.all([ + joinRoom(io, newChat._id as string, memberId), + joinRoom(io, newChat._id as string, senderId), + User.updateMany( + { _id: { $in: [memberId, senderId] } }, + { $push: { chats: { chat: newChat._id } } }, + { new: true } + ), + ]); + socket + .to(newChat._id as string) + .emit('JOIN_PRIVATE_CHAT', { chatId: newChat._id as string }); + + ack({ + success: true, + message: 'Chat created successfuly', + data: newChat, + }); +}; + +const handleCreateGroupChannel = async ( + io: any, + socket: Socket, + data: any, + ack: Function, + senderId: any +) => { + const { type, name, members } = data; + const user = User.findById(senderId); + if (!user) + return ack({ + success: false, + message: 'Faild to create the chat', + error: 'you need to login first', + }); + + if (!process.env.GROUP_SIZE) + return ack({ + success: false, + message: 'Faild to create the chat', + error: 'define GROUP_SIZE in your .env file', + }); + + if (type === 'group' && members.length > process.env.GROUP_SIZE) + return ack({ + success: false, + message: 'Faild to create the chat', + error: `groups cannot have more than ${process.env.GROUP_SIZE} members`, + }); + + const membersWithRoles = members.map((id: ObjectId) => ({ + user: id, + Role: 'member', + })); + const allMembers = [ + ...membersWithRoles, + { + user: senderId, + Role: 'admin', + }, + ]; + const newChat = new GroupChannel({ + name, + type, + members: allMembers, + }); + await newChat.save(); + await Promise.all([ + allMembers.map((member) => + joinRoom(io, newChat._id as string, member.user) + ), + User.updateMany( + { _id: { $in: [allMembers.map((member) => member.user)] } }, + { $push: { chats: { chat: newChat._id } } }, + { new: true } + ), + ]); + socket + .to(newChat._id as string) + .emit('JOIN_GROUP_CHANNEL', { chatId: newChat._id as string }); + + ack({ + success: true, + message: 'Chat created successfuly', + data: newChat, + }); +}; + +const handleDeleteGroupChannel = async ( + io: any, + socket: Socket, + data: any, + ack: Function, + senderId: any +) => { + const { chatId } = data; + const chat = await Chat.findById(chatId); + + if (!chat || chat.isDeleted) + return ack({ + success: false, + message: 'Could not delete the group', + error: 'no chat found with the provided id', + }); + + const chatMembers = chat.members; + const isCreator = chatMembers.some( + (member) => member.user.toString() === senderId && member.Role === 'admin' + ); + + if (!isCreator) + return ack({ + success: false, + message: 'Could not delete the group', + error: 'you are not authorized to delete the group', + }); + + chatMembers.map(async (member: any) => { + await inform(io, member.user, { chatId }, 'DELETE_GROUP_CHANNEL_SERVER'); + }); + + chat.members = []; + chat.isDeleted = true; + await chat.save(); + + return ack({ + success: true, + message: 'chat deleted successfuly', + data: chatId, + }); +}; + +const handleLeaveGroupChannel = async ( + io: any, + socket: Socket, + data: any, + ack: Function, + senderId: any +) => { + const { chatId } = data; + const chat = await Chat.findById(chatId); + if (!chat || chat.isDeleted) + return ack({ + success: false, + message: 'could not leave the group', + error: 'this chat does no longer exist', + }); + const isMember = chat.members.some( + (member: any) => member.user.toString() === senderId.toString() + ); + if (!isMember) + return ack({ + success: false, + message: 'could not leave the group', + error: 'you are not a member of this chat', + }); + + await Chat.updateOne( + { _id: chatId }, + { $pull: { members: { user: senderId } } } + ); + + socket + .to(chatId) + .emit('LEAVE_GROUP_CHANNEL_SERVER', { chatId, memberId: senderId }); + ack({ + success: true, + message: 'left the group successfuly', + data: {}, + }); +}; + +const handleRemoveMembers = async ( + io: any, + socket: Socket, + data: any, + ack: Function, + senderId: any +) => { + const { chatId, members } = data; + const forbiddenUsers: string[] = []; + + const chat = await Chat.findById(chatId); + if (!chat || chat.isDeleted) + return ack({ + success: false, + message: 'could not remove members from the group', + error: 'this chat does no longer exist', + }); + + const admin: Member = chat.members.find((m) => + m.user.equals(senderId) + ) as unknown as Member; + + if (!admin) + return ack({ + success: false, + message: 'could not remove members from the group', + error: 'you are no longer a member of this group', + }); + + if (admin.Role === 'member') + return ack({ + success: false, + message: 'could not remove members from the group', + error: 'you do not have permission', + }); + + await Promise.all( + members.map(async (memberId: any) => { + const user = await User.findById(memberId); + if (!user) { + forbiddenUsers.push(memberId); + return; + } + + const isMember = chat.members.some((m: any) => m.user.equals(memberId)); + if (!isMember) { + forbiddenUsers.push(memberId); + return; + } + + await Chat.updateOne( + { _id: chatId }, + { $pull: { members: { user: memberId } } } + ); + + await inform(io, memberId, { chatId }, 'REMOVE_MEMBERS_SERVER'); + }) + ); + if (forbiddenUsers.length > 0) + return ack({ + success: false, + message: 'Some users could not be added', + error: `Could not remove users with IDs: ${forbiddenUsers.join(', ')}`, + }); + ack({ + success: true, + message: 'Members removed successfully', + data: {}, + }); +}; + +const handleSetPermission = async ( + io: any, + socket: Socket, + data: any, + ack: Function, + senderId: any +) => { + const { chatId, type, who } = data; + + if (!type || !who) + return ack({ + success: false, + message: 'could not update permissions', + error: 'missing required fields', + }); + + const chat = await GroupChannel.findById(chatId); + if (!chat || chat.isDeleted) + return ack({ + success: false, + message: 'could not update permissions', + error: 'this chat does no longer exist', + }); + + if (chat.type === 'private') + return ack({ + success: false, + message: 'could not update permissions', + error: 'cannot change permissions for private chats', + }); + + const admin: Member = chat.members.find((m: any) => + m.user.equals(senderId) + ) as unknown as Member; + + if (!admin) + return ack({ + success: false, + message: 'could not update permissions', + error: 'you are no longer a member of this group', + }); + + if (admin.Role === 'member') + return ack({ + success: false, + message: 'could not change group permissions', + error: 'you do not have permission', + }); + + if (type === 'post') chat.messagingPermission = who === 'everyone'; + else if (type === 'download') chat.downloadingPermission = who === 'everyone'; + await chat.save(); + socket.to(chatId).emit('SET_PERMISSION_SERVER', { chatId, type, who }); + console.log(chat); + ack({ + success: true, + message: 'permissions updated successfully', + data: {}, + }); +}; + +const handleDraftMessage = async ( io: Server, socket: Socket, data: any, @@ -14,6 +484,38 @@ export const handleDraftMessage = async ( }; const registerChatHandlers = (io: Server, socket: Socket, userId: any) => { + socket.on('CREATE_PRIVATE_CHAT', (data: any, ack: Function) => { + handleCreatePrivateChat(io, socket, data, ack, userId); + }); + + socket.on('CREATE_GROUP_CHANNEL', (data: any, ack: Function) => { + handleCreateGroupChannel(io, socket, data, ack, userId); + }); + + socket.on('DELETE_GROUP_CHANNEL_CLIENT', (data: any, ack: Function) => { + handleDeleteGroupChannel(io, socket, data, ack, userId); + }); + + socket.on('LEAVE_GROUP_CHANNEL_CLIENT', (data: any, ack: Function) => { + handleLeaveGroupChannel(io, socket, data, ack, userId); + }); + + socket.on('SET_PERMISSION_CLIENT', (data: any, ack: Function) => { + handleSetPermission(io, socket, data, ack, userId); + }); + + socket.on('ADD_ADMINS_CLIENT', (data: any, ack: Function) => { + handleAddAdmins(io, data, ack, userId); + }); + + socket.on('ADD_MEMBERS_CLIENT', (data: any, ack: Function) => { + handleAddMembers(io, data, ack, userId); + }); + + socket.on('REMOVE_MEMBERS_CLIENT', (data: any, ack: Function) => { + handleRemoveMembers(io, socket, data, ack, userId); + }); + socket.on('UPDATE_DRAFT_CLIENT', (data: any, ack: Function) => handleDraftMessage(io, socket, data, ack, userId) ); diff --git a/src/sockets/messages.ts b/src/sockets/messages.ts index 7c86476..7a17349 100644 --- a/src/sockets/messages.ts +++ b/src/sockets/messages.ts @@ -1,12 +1,208 @@ -import Message from '@base/models/messageModel'; -import mongoose from 'mongoose'; +import { Types } from 'mongoose'; import { Server, Socket } from 'socket.io'; +import IMessage from '@base/types/message'; +import Message from '@models/messageModel'; +import NormalChat from '@models/normalChatModel'; +import User from '@models/userModel'; +import GroupChannel from '@models/groupChannelModel'; +import { enableDestruction } from '@services/chatService'; +import { joinRoom, updateDraft } from './MessagingServices'; interface PinUnPinMessageData { - chatId: string | mongoose.Types.ObjectId; - messageId: string | mongoose.Types.ObjectId; + chatId: string | Types.ObjectId; + messageId: string | Types.ObjectId; } +export const handleMessaging = async ( + io: any, + socket: Socket, + data: any, + ack: Function, + senderId: string +) => { + let { chatId, media, content, contentType, parentMessageId } = data; + const { isFirstTime, chatType, isReply, isForward } = data; + if ( + (!isForward && + !content && + !media && + (!contentType || !chatType || !chatId)) || + ((isReply || isForward) && !parentMessageId) + ) + return ack({ + success: false, + message: 'Failed to send the message', + error: 'missing required Fields', + }); + + if ( + (isFirstTime && isReply) || + (isForward && (content || media || contentType)) + ) + return ack({ + success: false, + message: 'Failed to send the message', + error: 'conflicting fields', + }); + if (isFirstTime) { + const members = [{ user: chatId }, { user: senderId }]; + const chat = new NormalChat({ members }); + const id = String(chat._id); + await chat.save(); + socket.join(id); + await joinRoom(io, id, chatId); + await User.findByIdAndUpdate(senderId, { + $push: { + chats: { chat: id }, + }, + }); + await User.findByIdAndUpdate(chatId, { + $push: { + chats: { chat: id }, + }, + }); + chatId = id; + } + + if (chatType !== 'private') { + const chat = await GroupChannel.findById(chatId); + if (!chat) + return ack({ + success: false, + message: 'Failed to send the message', + error: 'this chat does not exist', + }); + const sender = chat.members.find((member: any) => + member.user.equals(senderId) + ); + console.log(sender); + if (!sender) + return ack({ + success: false, + message: 'Failed to send the message', + error: 'you are not a member of this chat', + }); + if (sender.Role !== 'admin' && !chat.messagnigPermission) + return ack({ + success: false, + message: 'Failed to send the message', + error: 'only admins can post and reply to this chat', + }); + if (sender.Role !== 'admin' && !isReply && chatType === 'channel') + return ack({ + success: false, + message: 'Failed to send the message', + error: 'only admins can post to this channel', + }); + } + + let parentMessage; + if (isForward || isReply) { + parentMessage = (await Message.findById(parentMessageId)) as IMessage; + if (!parentMessage) + return ack({ + success: false, + message: 'Failed to send the message', + error: 'No message found with the provided id', + }); + + if (!parentMessage) + return ack({ + success: false, + message: 'Failed to reply to the message', + error: 'No message found with the provided parent message id', + }); + + if (isForward) { + ({ content, contentType, media } = parentMessage); + parentMessageId = undefined; + } + } + + const message = new Message({ + content, + contentType, + isForward, + senderId, + chatId, + parentMessageId, + messageType: chatType, + }); + await message.save(); + if (parentMessage && isReply && chatType === 'channel') { + parentMessage.threadMessages.push(message._id as Types.ObjectId); + await parentMessage.save(); + } + + await updateDraft(io, senderId, chatId, ''); + socket.to(chatId).emit('RECEIVE_MESSAGE', message); + const res = { + messageId: message._id, + }; + enableDestruction(socket, message, chatId); + ack({ success: true, message: 'Message sent successfully', res }); +}; + +export const handleEditMessage = async ( + socket: Socket, + data: any, + ack: Function +) => { + const { messageId, content, chatId } = data; + if (!messageId || !content) + return ack({ + success: false, + message: 'Failed to edit the message', + error: 'missing required Fields', + }); + const message = await Message.findByIdAndUpdate( + messageId, + { content, isEdited: true }, + { new: true } + ); + if (!message) + return ack({ + success: false, + message: 'Failed to edit the message', + error: 'no message found with the provided id', + }); + if (message.isForward) + return ack({ + success: false, + message: 'Failed to edit the message', + error: 'cannot edit a forwarded message', + }); + socket.to(chatId).emit('EDIT_MESSAGE_SERVER', message); + ack({ + success: true, + message: 'Message edited successfully', + res: { message }, + }); +}; + +export const handleDeleteMessage = async ( + socket: Socket, + data: any, + ack: Function +) => { + const { messageId, chatId } = data; + if (!messageId) + return ack({ + success: false, + message: 'Failed to delete the message', + error: 'missing required Fields', + }); + const message = await Message.findByIdAndDelete(messageId); + if (!message) + return ack({ + success: false, + message: 'Failed to delete the message', + error: 'no message found with the provided id', + }); + socket.to(chatId).emit('DELETE_MESSAGE_SERVER', messageId); + ack({ success: true, message: 'Message deleted successfully' }); +}; + async function handlePinMessage(socket: Socket, data: PinUnPinMessageData) { try { // Make a message pinned @@ -45,7 +241,23 @@ async function handleUnPinMessage(socket: Socket, data: PinUnPinMessageData) { } } -async function registerMessagesHandlers(io: Server, socket: Socket, _userId: string) { +async function registerMessagesHandlers( + io: Server, + socket: Socket, + userId: string +) { + socket.on('SEND_MESSAGE', (data: any, ack: Function) => + handleMessaging(io, socket, data, ack, userId) + ); + + socket.on('EDIT_MESSAGE_CLIENT', (data: any, ack: Function) => + handleEditMessage(socket, data, ack) + ); + + socket.on('DELETE_MESSAGE_CLIENT', (data: any, ack: Function) => + handleDeleteMessage(socket, data, ack) + ); + socket.on('PIN_MESSAGE_CLIENT', (data: PinUnPinMessageData) => handlePinMessage(socket, data) ); diff --git a/src/sockets/services.ts b/src/sockets/services.ts deleted file mode 100644 index 0ab904c..0000000 --- a/src/sockets/services.ts +++ /dev/null @@ -1,694 +0,0 @@ -import { Socket } from 'socket.io'; -import Message from '@base/models/messageModel'; -import { enableDestruction, getChatIds } from '@services/chatService'; -import IMessage from '@base/types/message'; -import NormalChat from '@base/models/normalChatModel'; -import mongoose, { ObjectId } from 'mongoose'; -import { getSocketsByUserId } from '@base/services/sessionService'; -import User from '@base/models/userModel'; -import Chat from '@base/models/chatModel'; -import GroupChannel from '@base/models/groupChannelModel'; - -interface Member { - _id: mongoose.Types.ObjectId; - Role: 'member' | 'admin'; -} - -const check = async (chatId: any, ack: Function, senderId: any) => { - if (!chatId) { - return ack({ success: false, message: 'provide the chatId' }); - } - const chat = await Chat.findById(chatId); - if (!chat) { - return ack({ - success: false, - message: 'no chat found with the provided id', - }); - } - - if (chat.type === 'private') - return ack({ - success: false, - message: 'this is a private chat!', - }); - const chatMembers = chat.members; - const chatMembersIds = chatMembers.map((m: any) => m._id); - if (chatMembersIds.length === 0) - return ack({ - success: false, - message: 'this chat is deleted and it no longer exists', - }); - - const admin: Member = chatMembers.find((m) => - m.user.equals(senderId) - ) as unknown as Member; - - if (!admin || admin.Role === 'member') - return ack({ - success: false, - message: 'you do not have permission', - }); -}; - -const inform = async (io: any, userId: string, data: any, event: string) => { - let memberSocket; - const socketIds = await getSocketsByUserId(userId); - if (!socketIds || socketIds.length !== 0) - socketIds.forEach((socketId: any) => { - memberSocket = io.sockets.sockets.get(socketId); - if (memberSocket) memberSocket.emit(event, data); - }); -}; - -const joinRoom = async (io: any, roomId: String, userId: ObjectId) => { - const socketIds = await getSocketsByUserId(userId); - socketIds.forEach((socketId: string) => { - const socket = io.sockets.sockets.get(socketId); - if (socket) socket.join(roomId); - }); -}; - -export const updateDraft = async ( - io: any, - senderId: string, - chatId: string, - content: string -) => { - await User.findByIdAndUpdate( - senderId, - { $set: { 'chats.$[chat].draft': content } }, - { - arrayFilters: [{ 'chat.chat': chatId }], - } - ); - await inform(io, senderId, { chatId, draft: content }, 'UPDATE_DRAFT_SERVER'); -}; - -export const joinAllRooms = async ( - socket: Socket, - userId: mongoose.Types.ObjectId -) => { - const chatIds = await getChatIds(userId); - chatIds.forEach((chatId: mongoose.Types.ObjectId) => { - socket.join(chatId.toString()); - }); -}; - -export const handleMessaging = async ( - io: any, - socket: Socket, - data: any, - ack: Function, - senderId: string -) => { - let { chatId, media, content, contentType, parentMessageId } = data; - const { isFirstTime, chatType, isReply, isForward } = data; - if ( - (!isForward && - !content && - !media && - (!contentType || !chatType || !chatId)) || - ((isReply || isForward) && !parentMessageId) - ) - return ack({ - success: false, - message: 'Failed to send the message', - error: 'missing required Fields', - }); - - if ( - (isFirstTime && isReply) || - (isForward && (content || media || contentType)) - ) - return ack({ - success: false, - message: 'Failed to send the message', - error: 'conflicting fields', - }); - if (isFirstTime) { - const members = [{ user: chatId }, { user: senderId }]; - const chat = new NormalChat({ members }); - const id = String(chat._id); - await chat.save(); - socket.join(id); - await joinRoom(io, id, chatId); - await User.findByIdAndUpdate(senderId, { - $push: { - chats: { chat: id }, - }, - }); - await User.findByIdAndUpdate(chatId, { - $push: { - chats: { chat: id }, - }, - }); - chatId = id; - } - - if (chatType !== 'private') { - const chat = await GroupChannel.findById(chatId); - if (!chat) - return ack({ - success: false, - message: 'Failed to send the message', - error: 'this chat does not exist', - }); - const sender = chat.members.find((member: any) => - member.user.equals(senderId) - ); - console.log(sender); - if (!sender) - return ack({ - success: false, - message: 'Failed to send the message', - error: 'you are not a member of this chat', - }); - if (sender.Role !== 'admin' && !chat.messagnigPermission) - return ack({ - success: false, - message: 'Failed to send the message', - error: 'only admins can post and reply to this chat', - }); - if (sender.Role !== 'admin' && !isReply && chatType === 'channel') - return ack({ - success: false, - message: 'Failed to send the message', - error: 'only admins can post to this channel', - }); - } - - let parentMessage; - if (isForward || isReply) { - parentMessage = (await Message.findById(parentMessageId)) as IMessage; - if (!parentMessage) - return ack({ - success: false, - message: 'Failed to send the message', - error: 'No message found with the provided id', - }); - - if (!parentMessage) - return ack({ - success: false, - message: 'Failed to reply to the message', - error: 'No message found with the provided parent message id', - }); - - if (isForward) { - ({ content, contentType, media } = parentMessage); - parentMessageId = undefined; - } - } - - const message = new Message({ - content, - contentType, - isForward, - senderId, - chatId, - parentMessageId, - messageType: chatType, - }); - await message.save(); - if (parentMessage && isReply && chatType === 'channel') { - parentMessage.threadMessages.push(message._id as mongoose.Types.ObjectId); - await parentMessage.save(); - } - - await updateDraft(io, senderId, chatId, ''); - socket.to(chatId).emit('RECEIVE_MESSAGE', message); - const res = { - messageId: message._id, - }; - enableDestruction(socket, message, chatId); - ack({ success: true, message: 'Message sent successfully', res }); -}; - -export const handleEditMessage = async ( - socket: Socket, - data: any, - ack: Function -) => { - const { messageId, content, chatId } = data; - if (!messageId || !content) - return ack({ - success: false, - message: 'Failed to edit the message', - error: 'missing required Fields', - }); - const message = await Message.findByIdAndUpdate( - messageId, - { content, isEdited: true }, - { new: true } - ); - if (!message) - return ack({ - success: false, - message: 'Failed to edit the message', - error: 'no message found with the provided id', - }); - if (message.isForward) - return ack({ - success: false, - message: 'Failed to edit the message', - error: 'cannot edit a forwarded message', - }); - socket.to(chatId).emit('EDIT_MESSAGE_SERVER', message); - ack({ - success: true, - message: 'Message edited successfully', - res: { message }, - }); -}; - -export const handleDeleteMessage = async ( - socket: Socket, - data: any, - ack: Function -) => { - const { messageId, chatId } = data; - if (!messageId) - return ack({ - success: false, - message: 'Failed to delete the message', - error: 'missing required Fields', - }); - const message = await Message.findByIdAndDelete(messageId); - if (!message) - return ack({ - success: false, - message: 'Failed to delete the message', - error: 'no message found with the provided id', - }); - socket.to(chatId).emit(messageId); - ack({ success: true, message: 'Message deleted successfully' }); -}; - -export const handleAddAdmins = async ( - io: any, - data: any, - ack: Function, - senderId: any -) => { - const { members, chatId } = data; - const forbiddenUsers: string[] = []; - const func = await check(chatId, ack, senderId); - const chat = await Chat.findById(chatId); - - if (func) return func; - - if (!chat || chat.isDeleted) - return ack({ - success: false, - message: 'Could not add admins to the group', - error: 'Chat not found', - }); - - await Promise.all( - members.map(async (memId: string) => { - const user = await User.findById(memId); - - if (!user) { - forbiddenUsers.push(memId); - return; - } - - const isMemberOfChat = chat.members.some((m) => m.user.equals(memId)); - - if (!isMemberOfChat) { - forbiddenUsers.push(memId); - return; - } - - await Chat.findByIdAndUpdate( - chatId, - { $set: { 'members.$[elem].Role': 'admin' } }, - { - new: true, - arrayFilters: [{ 'elem.user': memId }], - } - ); - - await inform(io, memId, { chatId }, 'ADD_ADMINS_SERVER'); - }) - ); - - if (forbiddenUsers.length > 0) { - return ack({ - success: false, - message: 'Some users could not be added as admins', - error: `Could not add users with IDs: ${forbiddenUsers.join(', ')}`, - }); - } - - ack({ - success: true, - message: 'Added admins successfully', - data: {}, - }); -}; - -export const handleAddMembers = async ( - io: any, - data: any, - ack: Function, - senderId: any -) => { - const { chatId, users } = data; - const forbiddenUsers: string[] = []; - - const func = await check(chatId, ack, senderId); - if (func) return func; - - const chat = await Chat.findById(chatId); - if (!chat || chat.isDeleted) - return ack({ - success: false, - message: 'No chat found with the provided ID', - }); - - await Promise.all( - users.map(async (userId: any) => { - const user = await User.findById(userId); - - if (!user) { - forbiddenUsers.push(userId); - return; - } - - const isAlreadyMember = chat.members.some((m: any) => - m.user.equals(userId) - ); - if (!isAlreadyMember) chat.members.push({ user: userId, Role: 'member' }); - const userWasMember = user.chats.some((c: any) => c.chat.equals(chatId)); - if (!userWasMember) user.chats.push(chatId); - console.log(user.chats); - if (isAlreadyMember) { - forbiddenUsers.push(userId); - return; - } - - await inform(io, userId, { chatId }, 'ADD_MEMBERS_SERVER'); - }) - ); - - if (forbiddenUsers.length > 0) { - return ack({ - success: false, - message: 'Some users could not be added', - error: `Could not add users with IDs: ${forbiddenUsers.join(', ')}`, - }); - } - - await chat.save(); - - ack({ - success: true, - message: 'Members added successfully', - data: {}, - }); -}; - -export const handleCreateGroupChannel = async ( - io: any, - socket: Socket, - data: any, - ack: Function, - senderId: any -) => { - const { type, name, members } = data; - const user = User.findById(senderId); - if (!user) - return ack({ - success: false, - message: 'Faild to create the chat', - error: 'you need to login first', - }); - - if (!process.env.GROUP_SIZE) - return ack({ - success: false, - message: 'Faild to create the chat', - error: 'define GROUP_SIZE in your .env file', - }); - - if (type === 'group' && members.length > process.env.GROUP_SIZE) - return ack({ - success: false, - message: 'Faild to create the chat', - error: `groups cannot have more than ${process.env.GROUP_SIZE} members`, - }); - - const membersWithRoles = members.map((id: ObjectId) => ({ - user: id, - Role: 'member', - })); - const allMembers = [ - ...membersWithRoles, - { - user: senderId, - Role: 'admin', - }, - ]; - const newChat = new GroupChannel({ - name, - type, - members: allMembers, - }); - await newChat.save(); - await Promise.all( - allMembers.map(async (member) => { - await joinRoom(io, newChat._id as string, member.user); - return User.findByIdAndUpdate( - member.user, - { $push: { chats: { chat: newChat._id } } }, - { new: true } - ); - }) - ); - socket - .to(newChat._id as string) - .emit('JOIN_GROUP_CHANNEL', { chatId: newChat._id as string }); - - ack({ - success: true, - message: 'Chat created successfuly', - data: newChat, - }); -}; - -export const handleDeleteGroupChannel = async ( - io: any, - socket: Socket, - data: any, - ack: Function, - senderId: any -) => { - const { chatId } = data; - const chat = await Chat.findById(chatId); - - if (!chat || chat.isDeleted) - return ack({ - success: false, - message: 'Could not delete the group', - error: 'no chat found with the provided id', - }); - - const chatMembers = chat.members; - const isCreator = chatMembers.some( - (member) => member.user.toString() === senderId && member.Role === 'admin' - ); - - if (!isCreator) - return ack({ - success: false, - message: 'Could not delete the group', - error: 'you are not authorized to delete the group', - }); - - chatMembers.map(async (member: any) => { - await inform(io, member.user, { chatId }, 'DELETE_GROUP_CHANNEL_SERVER'); - }); - - chat.members = []; - chat.isDeleted = true; - await chat.save(); - - return ack({ - success: true, - message: 'chat deleted successfuly', - data: chatId, - }); -}; - -export const handleLeaveGroupChannel = async ( - io: any, - socket: Socket, - data: any, - ack: Function, - senderId: any -) => { - const { chatId } = data; - const chat = await Chat.findById(chatId); - if (!chat || chat.isDeleted) - return ack({ - success: false, - message: 'could not leave the group', - error: 'this chat does no longer exist', - }); - const isMember = chat.members.some( - (member: any) => member.user.toString() === senderId.toString() - ); - if (!isMember) - return ack({ - success: false, - message: 'could not leave the group', - error: 'you are not a member of this chat', - }); - - await Chat.updateOne( - { _id: chatId }, - { $pull: { members: { user: senderId } } } - ); - - socket - .to(chatId) - .emit('LEAVE_GROUP_CHANNEL_SERVER', { chatId, memberId: senderId }); - ack({ - success: true, - message: 'left the group successfuly', - data: {}, - }); -}; - -export const handleRemoveMembers = async ( - io: any, - socket: Socket, - data: any, - ack: Function, - senderId: any -) => { - const { chatId, members } = data; - const forbiddenUsers: string[] = []; - - const chat = await Chat.findById(chatId); - if (!chat || chat.isDeleted) - return ack({ - success: false, - message: 'could not remove members from the group', - error: 'this chat does no longer exist', - }); - - const admin: Member = chat.members.find((m) => - m.user.equals(senderId) - ) as unknown as Member; - - if (!admin) - return ack({ - success: false, - message: 'could not remove members from the group', - error: 'you are no longer a member of this group', - }); - - if (admin.Role === 'member') - return ack({ - success: false, - message: 'could not remove members from the group', - error: 'you do not have permission', - }); - - await Promise.all( - members.map(async (memberId: any) => { - const user = await User.findById(memberId); - if (!user) { - forbiddenUsers.push(memberId); - return; - } - - const isMember = chat.members.some((m: any) => m.user.equals(memberId)); - if (!isMember) { - forbiddenUsers.push(memberId); - return; - } - - await Chat.updateOne( - { _id: chatId }, - { $pull: { members: { user: memberId } } } - ); - - await inform(io, memberId, { chatId }, 'REMOVE_MEMBERS_SERVER'); - }) - ); - if (forbiddenUsers.length > 0) - return ack({ - success: false, - message: 'Some users could not be added', - error: `Could not remove users with IDs: ${forbiddenUsers.join(', ')}`, - }); - ack({ - success: true, - message: 'Members removed successfully', - data: {}, - }); -}; - -export const handleSetPermission = async ( - io: any, - socket: Socket, - data: any, - ack: Function, - senderId: any -) => { - const { chatId, type, who } = data; - - if (!type || !who) - return ack({ - success: false, - message: 'could not update permissions', - error: 'missing required fields', - }); - - const chat = await GroupChannel.findById(chatId); - if (!chat || chat.isDeleted) - return ack({ - success: false, - message: 'could not update permissions', - error: 'this chat does no longer exist', - }); - - if (chat.type === 'private') - return ack({ - success: false, - message: 'could not update permissions', - error: 'cannot change permissions for private chats', - }); - - const admin: Member = chat.members.find((m: any) => - m.user.equals(senderId) - ) as unknown as Member; - - if (!admin) - return ack({ - success: false, - message: 'could not update permissions', - error: 'you are no longer a member of this group', - }); - - if (admin.Role === 'member') - return ack({ - success: false, - message: 'could not change group permissions', - error: 'you do not have permission', - }); - - if (type === 'post') chat.messagingPermission = who === 'everyone'; - else if (type === 'download') chat.downloadingPermission = who === 'everyone'; - await chat.save(); - socket.to(chatId).emit('SET_PERMISSION_SERVER', { chatId, type, who }); - console.log(chat); - ack({ - success: true, - message: 'permissions updated successfully', - data: {}, - }); -}; diff --git a/src/sockets/socket.ts b/src/sockets/socket.ts index 46b8c90..734ec92 100644 --- a/src/sockets/socket.ts +++ b/src/sockets/socket.ts @@ -3,20 +3,8 @@ import { Server as HTTPServer } from 'http'; import corsOptions from '@base/config/cors'; import registerChatHandlers from '@base/sockets/chat'; import redisClient from '@base/config/redis'; -import mongoose from 'mongoose'; -import { - joinAllRooms, - handleEditMessage, - handleDeleteMessage, - handleMessaging, - handleAddAdmins, - handleCreateGroupChannel, - handleDeleteGroupChannel, - handleLeaveGroupChannel, - handleAddMembers, - handleRemoveMembers, - handleSetPermission, -} from './services'; +import { Types } from 'mongoose'; +import { joinAllRooms } from './MessagingServices'; import registerMessagesHandlers from './messages'; import { authorizeSocket, protectSocket } from './middlewares'; import registerVoiceCallHandlers from './voiceCalls'; @@ -32,51 +20,7 @@ const socketSetup = (server: HTTPServer) => { io.on('connection', async (socket: any) => { const userId = socket.request.session.user.id; console.log(`New client with userID ${userId} connected: ${socket.id}`); - await joinAllRooms(socket, new mongoose.Types.ObjectId(userId as string)); - - socket.on('SEND_MESSAGE', (data: any, ack: Function) => - handleMessaging(io, socket, data, ack, userId) - ); - - socket.on('EDIT_MESSAGE_CLIENT', (data: any, ack: Function) => - handleEditMessage(socket, data, ack) - ); - - socket.on('DELETE_MESSAGE', (data: any, ack: Function) => - handleDeleteMessage(socket, data, ack) - ); - - socket.on('ADD_ADMINS_CLIENT', (data: any, ack: Function) => { - handleAddAdmins(io, data, ack, userId); - }); - - socket.on('ADD_MEMBERS_CLIENT', (data: any, ack: Function) => { - handleAddMembers(io, data, ack, userId); - }); - - socket.on('CREATE_GROUP_CHANNEL', (data: any, ack: Function) => { - handleCreateGroupChannel(io, socket, data, ack, userId); - }); - - socket.on('DELETE_GROUP_CHANNEL_CLIENT', (data: any, ack: Function) => { - handleDeleteGroupChannel(io, socket, data, ack, userId); - }); - - socket.on('LEAVE_GROUP_CHANNEL_CLIENT', (data: any, ack: Function) => { - handleLeaveGroupChannel(io, socket, data, ack, userId); - }); - - socket.on('REMOVE_MEMBERS_CLIENT', (data: any, ack: Function) => { - handleRemoveMembers(io, socket, data, ack, userId); - }); - - socket.on('REMOVE_MEMBERS_CLIENT', (data: any, ack: Function) => { - handleRemoveMembers(io, socket, data, ack, userId); - }); - - socket.on('SET_PERMISSION_CLIENT', (data: any, ack: Function) => { - handleSetPermission(io, socket, data, ack, userId); - }); + await joinAllRooms(socket, new Types.ObjectId(userId as string)); socket.on('error', (error: Error) => { console.error(`Socket error on ${socket.id}:`, error); diff --git a/src/utils/email.ts b/src/utils/email.ts index d96dcae..356ff62 100644 --- a/src/utils/email.ts +++ b/src/utils/email.ts @@ -3,7 +3,7 @@ import { MailOptions } from 'nodemailer/lib/json-transport'; const telwareTeam: string = 'Telware '; -const createTransporter = (provider: string) => { +const createTransporter = (provider?: string) => { if (provider === 'gmail') return nodemailer.createTransport({ service: 'gmail', @@ -14,17 +14,17 @@ const createTransporter = (provider: string) => { }); return nodemailer.createTransport({ - host: process.env.GMAIL_HOST, + host: process.env.MAILTRAP_HOST, port: Number(process.env.MAIL_PORT), auth: { - user: process.env.TELWARE_EMAIL, - pass: process.env.TELWARE_PASSWORD, + user: process.env.MAILTRAP_USERNAME, + pass: process.env.MAILTRAP_PASSWORD, }, }); }; const sendEmail = async (options: any) => { - const transporter = createTransporter('gmail'); + const transporter = createTransporter(process.env.EMAIL_PROVIDER); const mailOptions: MailOptions = { from: telwareTeam, From ae43842a85a492eac36cd2dd2e48b96c623b5b69 Mon Sep 17 00:00:00 2001 From: AhmedHamed3699 Date: Mon, 16 Dec 2024 17:57:34 +0200 Subject: [PATCH 15/54] hotfix(messaging): new messages doesn't appear when refresh --- src/controllers/chatController.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/controllers/chatController.ts b/src/controllers/chatController.ts index 6a82d5c..1c297ff 100644 --- a/src/controllers/chatController.ts +++ b/src/controllers/chatController.ts @@ -68,14 +68,16 @@ export const getMessages = catchAsync( } const messages = await Message.find(filter) - .limit(limit) - .sort({ timestamp: 1 }); + .sort({ timestamp: -1 }) + .limit(limit); if (!messages || messages.length === 0) { return next(new AppError('No messages found', 404)); } + + messages.reverse(); const nextPage = - messages.length < limit ? undefined : messages[messages.length - 1]._id; + messages.length < limit ? undefined : messages[0]._id; res.status(200).json({ status: 'success', From c50d908e5971cbc0c2fb3fecfc9659e3d02560a9 Mon Sep 17 00:00:00 2001 From: AhmedHamed3699 Date: Mon, 16 Dec 2024 18:31:15 +0200 Subject: [PATCH 16/54] hotfix(chats): solved creation error --- src/sockets/chat.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/sockets/chat.ts b/src/sockets/chat.ts index cfafaed..f3570cf 100644 --- a/src/sockets/chat.ts +++ b/src/sockets/chat.ts @@ -1,5 +1,5 @@ +import { Types } from 'mongoose'; import { Server, Socket } from 'socket.io'; -import { ObjectId } from 'mongodb'; import User from '@models/userModel'; import Chat from '@models/chatModel'; import GroupChannel from '@models/groupChannelModel'; @@ -217,7 +217,7 @@ const handleCreateGroupChannel = async ( error: `groups cannot have more than ${process.env.GROUP_SIZE} members`, }); - const membersWithRoles = members.map((id: ObjectId) => ({ + const membersWithRoles = members.map((id: Types.ObjectId) => ({ user: id, Role: 'member', })); @@ -239,7 +239,7 @@ const handleCreateGroupChannel = async ( joinRoom(io, newChat._id as string, member.user) ), User.updateMany( - { _id: { $in: [allMembers.map((member) => member.user)] } }, + { _id: { $in: allMembers.map((member) => member.user) } }, { $push: { chats: { chat: newChat._id } } }, { new: true } ), From fb0a15ce85dc7164b545556262ca45125d311064 Mon Sep 17 00:00:00 2001 From: AhmedHamed3699 Date: Tue, 17 Dec 2024 00:56:43 +0200 Subject: [PATCH 17/54] fix: solved some small bugs --- docs/api/chat.swagger.ts | 24 ++++----- docs/api/sockets.swagger.ts | 7 --- src/models/communicationModel.ts | 2 +- src/models/userModel.ts | 2 +- src/services/chatService.ts | 3 +- src/sockets/MessagingServices.ts | 87 ++++++++++++++++++++++---------- src/sockets/chat.ts | 53 ++++++++++--------- src/sockets/messages.ts | 78 ++++------------------------ src/types/message.ts | 1 - 9 files changed, 112 insertions(+), 145 deletions(-) diff --git a/docs/api/chat.swagger.ts b/docs/api/chat.swagger.ts index daa0f28..4feae81 100644 --- a/docs/api/chat.swagger.ts +++ b/docs/api/chat.swagger.ts @@ -315,9 +315,9 @@ * name: page * required: false * schema: - * type: integer - * example: 1 - * description: The page number for paginated messages (default is 1). + * type: objectId + * example: "674cbbba97faf0d2e8a93846" + * description: The page starting after that messageId. * - in: query * name: limit * required: false @@ -325,6 +325,13 @@ * type: integer * example: 50 * description: The number of messages to retrieve per page (default is 100). + * - in: query + * name: timestamp + * required: false + * schema: + * type: Date + * example: 2024-12-01T19:37:56.399Z + * description: The timestamp to retrieve messages after. * responses: * 200: * description: Messages retrieved successfully. @@ -383,17 +390,8 @@ * items: * type: string * example: [] - * messageType: - * type: string - * example: private - * __v: - * type: integer - * example: 0 - * id: - * type: string - * example: "674cbbba97faf0d2e8a93846" * nextPage: - * type: integer + * type: objectId * example: 2 * 400: * description: Bad Request - Chat does not exist. diff --git a/docs/api/sockets.swagger.ts b/docs/api/sockets.swagger.ts index 466f9e1..dd91af8 100644 --- a/docs/api/sockets.swagger.ts +++ b/docs/api/sockets.swagger.ts @@ -39,13 +39,6 @@ * type: string * nullable: true * description: ID of the parent message for reply or forward. - * isFirstTime: - * type: boolean - * description: Indicates whether the chat is being initiated for the first time. - * chatType: - * type: string - * enum: [group, direct, channel] - * description: Type of the chat. * isReply: * type: boolean * description: Indicates if the message is a reply. diff --git a/src/models/communicationModel.ts b/src/models/communicationModel.ts index 42bf1e0..289a043 100644 --- a/src/models/communicationModel.ts +++ b/src/models/communicationModel.ts @@ -33,7 +33,7 @@ const communicationSchema = new mongoose.Schema( } ); -communicationSchema.index({ timestamp: 1 }, { unique: true, background: true }); +communicationSchema.index({ timestamp: -1 }, { unique: true, background: true }); const Communication = mongoose.model('Communication', communicationSchema); export default Communication; diff --git a/src/models/userModel.ts b/src/models/userModel.ts index 42f27d1..24be644 100644 --- a/src/models/userModel.ts +++ b/src/models/userModel.ts @@ -235,7 +235,7 @@ const userSchema = new mongoose.Schema( //TODO: unreadMessages virtual property -userSchema.index({ email: 1 }, { unique: true, background: true }); +userSchema.index({ email: 1 }, { background: true }); userSchema.pre('save', async function (next) { if (!this.isModified('password') || !this.password) return next(); diff --git a/src/services/chatService.ts b/src/services/chatService.ts index d668016..8b36d86 100644 --- a/src/services/chatService.ts +++ b/src/services/chatService.ts @@ -34,6 +34,7 @@ export const getChats = async ( match: type ? { type } : {}, }); if (!userChats) return []; + //!FIXME: a33333333333333333333333333333333333333333333333333333333333333333333333333 return userChats.chats.filter((chat) => chat.chat !== null); }; @@ -55,7 +56,7 @@ export const enableDestruction = async ( if (chat && chat.destructionDuration) { setTimeout(async () => { await Message.findByIdAndDelete(messageId); - socket.to(chatId).emit('DESTRUCT_MESSAGE', messageId); + socket.to(chatId).emit('DELETE_MESSAGE_SERVER', messageId); }, chat.destructionDuration * 1000); } }; diff --git a/src/sockets/MessagingServices.ts b/src/sockets/MessagingServices.ts index 5caee2c..4afc017 100644 --- a/src/sockets/MessagingServices.ts +++ b/src/sockets/MessagingServices.ts @@ -3,50 +3,79 @@ import { getChatIds } from '@services/chatService'; import { Types } from 'mongoose'; import { getSocketsByUserId } from '@base/services/sessionService'; import User from '@base/models/userModel'; -import Chat from '@base/models/chatModel'; +import GroupChannel from '@base/models/groupChannelModel'; export interface Member { - _id: Types.ObjectId; + user: Types.ObjectId; Role: 'member' | 'admin'; } -export const check = async (chatId: any, ack: Function, senderId: any) => { - if (!chatId) { - return ack({ success: false, message: 'provide the chatId' }); - } - const chat = await Chat.findById(chatId); - if (!chat) { +export const check = async ( + chat: any, + ack: Function, + senderId: any, + additionalData?: any +) => { + const { chatType, checkAdmin, newMessageIsReply } = additionalData; + + if (!chat || chat.isDeleted) { return ack({ success: false, - message: 'no chat found with the provided id', + message: 'Chat not found', }); } - if (chat.type === 'private') - return ack({ - success: false, - message: 'this is a private chat!', - }); const chatMembers = chat.members; - const chatMembersIds = chatMembers.map((m: any) => m._id); - if (chatMembersIds.length === 0) + if (chatMembers.length === 0) return ack({ success: false, message: 'this chat is deleted and it no longer exists', }); - const admin: Member = chatMembers.find((m) => + const sender: Member = chatMembers.find((m: Member) => m.user.equals(senderId) ) as unknown as Member; - if (!admin || admin.Role === 'member') + if (!sender) + return ack({ + success: false, + message: 'you are not a member of this chat', + }); + + if (chatType && !chatType.includes(chat.type)) + return ack({ + success: false, + message: `this is a ${chat.type} chat!`, + }); + + if (checkAdmin && sender.Role !== 'admin') return ack({ success: false, - message: 'you do not have permission', + message: 'you do not have permission as you are not an admin', }); + + if (sender.Role !== 'admin' && chat.type !== 'private') { + const groupChannelChat = await GroupChannel.findById(chat._id); + + if (!groupChannelChat.messagnigPermission) + return ack({ + success: false, + message: 'only admins can post and reply to this chat', + }); + if (chat.type === 'channel' && !newMessageIsReply) + return ack({ + success: false, + message: 'only admins can post to this channel', + }); + } }; -export const inform = async (io: any, userId: string, data: any, event: string) => { +export const informSessions = async ( + io: any, + userId: string, + data: any, + event: string +) => { let memberSocket; const socketIds = await getSocketsByUserId(userId); if (!socketIds || socketIds.length !== 0) @@ -56,7 +85,11 @@ export const inform = async (io: any, userId: string, data: any, event: string) }); }; -export const joinRoom = async (io: any, roomId: String, userId: Types.ObjectId) => { +export const joinRoom = async ( + io: any, + roomId: String, + userId: Types.ObjectId +) => { const socketIds = await getSocketsByUserId(userId); socketIds.forEach((socketId: string) => { const socket = io.sockets.sockets.get(socketId); @@ -77,13 +110,15 @@ export const updateDraft = async ( arrayFilters: [{ 'chat.chat': chatId }], } ); - await inform(io, senderId, { chatId, draft: content }, 'UPDATE_DRAFT_SERVER'); + await informSessions( + io, + senderId, + { chatId, draft: content }, + 'UPDATE_DRAFT_SERVER' + ); }; -export const joinAllRooms = async ( - socket: Socket, - userId: Types.ObjectId -) => { +export const joinAllRooms = async (socket: Socket, userId: Types.ObjectId) => { const chatIds = await getChatIds(userId); chatIds.forEach((chatId: Types.ObjectId) => { socket.join(chatId.toString()); diff --git a/src/sockets/chat.ts b/src/sockets/chat.ts index f3570cf..b8308c0 100644 --- a/src/sockets/chat.ts +++ b/src/sockets/chat.ts @@ -6,7 +6,7 @@ import GroupChannel from '@models/groupChannelModel'; import NormalChat from '@base/models/normalChatModel'; import { check, - inform, + informSessions, joinRoom, Member, updateDraft, @@ -20,18 +20,14 @@ const handleAddAdmins = async ( ) => { const { members, chatId } = data; const forbiddenUsers: string[] = []; - const func = await check(chatId, ack, senderId); - const chat = await Chat.findById(chatId); + const chat = await Chat.findById(chatId); + const func = await check(chat, ack, senderId, { + chatType: ['group', 'channel'], + checkAdmin: true, + }); if (func) return func; - if (!chat || chat.isDeleted) - return ack({ - success: false, - message: 'Could not add admins to the group', - error: 'Chat not found', - }); - await Promise.all( members.map(async (memId: string) => { const user = await User.findById(memId); @@ -41,7 +37,7 @@ const handleAddAdmins = async ( return; } - const isMemberOfChat = chat.members.some((m) => m.user.equals(memId)); + const isMemberOfChat = chat?.members.some((m) => m.user.equals(memId)); if (!isMemberOfChat) { forbiddenUsers.push(memId); @@ -57,7 +53,7 @@ const handleAddAdmins = async ( } ); - await inform(io, memId, { chatId }, 'ADD_ADMINS_SERVER'); + await informSessions(io, memId, { chatId }, 'ADD_ADMINS_SERVER'); }) ); @@ -85,15 +81,12 @@ const handleAddMembers = async ( const { chatId, users } = data; const forbiddenUsers: string[] = []; - const func = await check(chatId, ack, senderId); - if (func) return func; - const chat = await Chat.findById(chatId); - if (!chat || chat.isDeleted) - return ack({ - success: false, - message: 'No chat found with the provided ID', - }); + const func = await check(chat, ack, senderId, { + chatType: ['group', 'channel'], + checkAdmin: true, + }); + if (func) return func; await Promise.all( users.map(async (userId: any) => { @@ -104,10 +97,11 @@ const handleAddMembers = async ( return; } - const isAlreadyMember = chat.members.some((m: any) => + const isAlreadyMember = chat?.members.some((m: any) => m.user.equals(userId) ); - if (!isAlreadyMember) chat.members.push({ user: userId, Role: 'member' }); + if (!isAlreadyMember) + chat?.members.push({ user: userId, Role: 'member' }); const userWasMember = user.chats.some((c: any) => c.chat.equals(chatId)); if (!userWasMember) user.chats.push(chatId); console.log(user.chats); @@ -116,7 +110,7 @@ const handleAddMembers = async ( return; } - await inform(io, userId, { chatId }, 'ADD_MEMBERS_SERVER'); + await informSessions(io, userId, { chatId }, 'ADD_MEMBERS_SERVER'); }) ); @@ -128,7 +122,7 @@ const handleAddMembers = async ( }); } - await chat.save(); + await chat?.save(); ack({ success: true, @@ -285,7 +279,12 @@ const handleDeleteGroupChannel = async ( }); chatMembers.map(async (member: any) => { - await inform(io, member.user, { chatId }, 'DELETE_GROUP_CHANNEL_SERVER'); + await informSessions( + io, + member.user, + { chatId }, + 'DELETE_GROUP_CHANNEL_SERVER' + ); }); chat.members = []; @@ -394,7 +393,7 @@ const handleRemoveMembers = async ( { $pull: { members: { user: memberId } } } ); - await inform(io, memberId, { chatId }, 'REMOVE_MEMBERS_SERVER'); + await informSessions(io, memberId, { chatId }, 'REMOVE_MEMBERS_SERVER'); }) ); if (forbiddenUsers.length > 0) @@ -441,7 +440,7 @@ const handleSetPermission = async ( error: 'cannot change permissions for private chats', }); - const admin: Member = chat.members.find((m: any) => + const admin: Member = chat.members.find((m: Member) => m.user.equals(senderId) ) as unknown as Member; diff --git a/src/sockets/messages.ts b/src/sockets/messages.ts index 7a17349..067432b 100644 --- a/src/sockets/messages.ts +++ b/src/sockets/messages.ts @@ -2,11 +2,9 @@ import { Types } from 'mongoose'; import { Server, Socket } from 'socket.io'; import IMessage from '@base/types/message'; import Message from '@models/messageModel'; -import NormalChat from '@models/normalChatModel'; -import User from '@models/userModel'; -import GroupChannel from '@models/groupChannelModel'; import { enableDestruction } from '@services/chatService'; -import { joinRoom, updateDraft } from './MessagingServices'; +import Chat from '@base/models/chatModel'; +import { check, updateDraft } from './MessagingServices'; interface PinUnPinMessageData { chatId: string | Types.ObjectId; @@ -20,8 +18,8 @@ export const handleMessaging = async ( ack: Function, senderId: string ) => { - let { chatId, media, content, contentType, parentMessageId } = data; - const { isFirstTime, chatType, isReply, isForward } = data; + let { media, content, contentType, parentMessageId } = data; + const { chatId, chatType, isReply, isForward } = data; if ( (!isForward && !content && @@ -35,66 +33,18 @@ export const handleMessaging = async ( error: 'missing required Fields', }); - if ( - (isFirstTime && isReply) || - (isForward && (content || media || contentType)) - ) + if (isForward && (content || media || contentType)) return ack({ success: false, message: 'Failed to send the message', error: 'conflicting fields', }); - if (isFirstTime) { - const members = [{ user: chatId }, { user: senderId }]; - const chat = new NormalChat({ members }); - const id = String(chat._id); - await chat.save(); - socket.join(id); - await joinRoom(io, id, chatId); - await User.findByIdAndUpdate(senderId, { - $push: { - chats: { chat: id }, - }, - }); - await User.findByIdAndUpdate(chatId, { - $push: { - chats: { chat: id }, - }, - }); - chatId = id; - } - if (chatType !== 'private') { - const chat = await GroupChannel.findById(chatId); - if (!chat) - return ack({ - success: false, - message: 'Failed to send the message', - error: 'this chat does not exist', - }); - const sender = chat.members.find((member: any) => - member.user.equals(senderId) - ); - console.log(sender); - if (!sender) - return ack({ - success: false, - message: 'Failed to send the message', - error: 'you are not a member of this chat', - }); - if (sender.Role !== 'admin' && !chat.messagnigPermission) - return ack({ - success: false, - message: 'Failed to send the message', - error: 'only admins can post and reply to this chat', - }); - if (sender.Role !== 'admin' && !isReply && chatType === 'channel') - return ack({ - success: false, - message: 'Failed to send the message', - error: 'only admins can post to this channel', - }); - } + const chat = await Chat.findById(chatId); + const func = await check(chat, ack, senderId, { + newMessageIsReply: isReply, + }); + if (func) return func; let parentMessage; if (isForward || isReply) { @@ -106,13 +56,6 @@ export const handleMessaging = async ( error: 'No message found with the provided id', }); - if (!parentMessage) - return ack({ - success: false, - message: 'Failed to reply to the message', - error: 'No message found with the provided parent message id', - }); - if (isForward) { ({ content, contentType, media } = parentMessage); parentMessageId = undefined; @@ -126,7 +69,6 @@ export const handleMessaging = async ( senderId, chatId, parentMessageId, - messageType: chatType, }); await message.save(); if (parentMessage && isReply && chatType === 'channel') { diff --git a/src/types/message.ts b/src/types/message.ts index 00f8289..9c4b707 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -14,7 +14,6 @@ interface IMessage extends ICommunication { chatId: Types.ObjectId; parentMessageId: Types.ObjectId | undefined; threadMessages: Types.ObjectId[]; - messageType: string; } export default IMessage; From b420c8f72f494e77f7b99bc3c4b4b5664284ed92 Mon Sep 17 00:00:00 2001 From: AhmedHamed3699 Date: Tue, 17 Dec 2024 03:56:14 +0200 Subject: [PATCH 18/54] feat(chats): added E2EE to private chats --- .env.example | 2 ++ src/controllers/chatController.ts | 3 +-- src/models/chatModel.ts | 8 ++++++++ src/models/normalChatModel.ts | 25 +++++++++++++++++++++++++ src/services/chatService.ts | 1 - src/sockets/{chat.ts => chats.ts} | 0 src/sockets/socket.ts | 2 +- src/types/normalChat.ts | 3 +++ src/utils/encryption.ts | 29 +++++++++++++++++++++++++++++ 9 files changed, 69 insertions(+), 4 deletions(-) rename src/sockets/{chat.ts => chats.ts} (100%) create mode 100644 src/utils/encryption.ts diff --git a/.env.example b/.env.example index 21d55e3..866168b 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,8 @@ REDIS_LOCALHOST_URL=redis://localhost:6379 SERVER_URL=http://localhost:3000 WEBSOCKET_URL=ws://localhost:3000 +ENCRYPTION_KEY_SECRET=encryption-key-secret + SESSION_SECRET=session-secret SESSION_EXPIRES_IN=180d diff --git a/src/controllers/chatController.ts b/src/controllers/chatController.ts index 1c297ff..5b3a6cb 100644 --- a/src/controllers/chatController.ts +++ b/src/controllers/chatController.ts @@ -76,8 +76,7 @@ export const getMessages = catchAsync( } messages.reverse(); - const nextPage = - messages.length < limit ? undefined : messages[0]._id; + const nextPage = messages.length < limit ? undefined : messages[0]._id; res.status(200).json({ status: 'success', diff --git a/src/models/chatModel.ts b/src/models/chatModel.ts index a36f57a..78ea4bf 100644 --- a/src/models/chatModel.ts +++ b/src/models/chatModel.ts @@ -1,5 +1,6 @@ import mongoose from 'mongoose'; import IChat from '@base/types/chat'; +import { decryptKey } from '@base/utils/encryption'; const chatSchema = new mongoose.Schema( { @@ -41,6 +42,13 @@ const chatSchema = new mongoose.Schema( delete member._id; }); } + if (ret.encryptionKey) { + ret.encryptionKey = decryptKey( + ret.encryptionKey, + ret.initializationVector, + ret.authTag + ); + } return ret; }, }, diff --git a/src/models/normalChatModel.ts b/src/models/normalChatModel.ts index 144f930..0ca9036 100644 --- a/src/models/normalChatModel.ts +++ b/src/models/normalChatModel.ts @@ -1,11 +1,36 @@ import mongoose from 'mongoose'; +import crypto from 'crypto'; +import { encryptKey } from '@utils/encryption'; import INormalChat from '@base/types/normalChat'; import Chat from './chatModel'; const normalChatSchema = new mongoose.Schema({ + encryptionKey: { + type: String, + default: crypto.randomBytes(32).toString('hex'), + }, + initializationVector: { + type: String, + default: crypto.randomBytes(12).toString('hex'), + }, + authTag: { + type: String, + default: '', + }, destructionTimestamp: Date, destructionDuration: Number, }); +normalChatSchema.pre('save', function (next) { + if (!this.isNew) return next(); + const { encryptedKey, authTag } = encryptKey( + this.encryptionKey, + this.initializationVector + ); + this.encryptionKey = encryptedKey; + this.authTag = authTag; + next(); +}); + const NormalChat = Chat.discriminator('NormalChat', normalChatSchema); export default NormalChat; diff --git a/src/services/chatService.ts b/src/services/chatService.ts index 8b36d86..ec590bb 100644 --- a/src/services/chatService.ts +++ b/src/services/chatService.ts @@ -34,7 +34,6 @@ export const getChats = async ( match: type ? { type } : {}, }); if (!userChats) return []; - //!FIXME: a33333333333333333333333333333333333333333333333333333333333333333333333333 return userChats.chats.filter((chat) => chat.chat !== null); }; diff --git a/src/sockets/chat.ts b/src/sockets/chats.ts similarity index 100% rename from src/sockets/chat.ts rename to src/sockets/chats.ts diff --git a/src/sockets/socket.ts b/src/sockets/socket.ts index 734ec92..43d5724 100644 --- a/src/sockets/socket.ts +++ b/src/sockets/socket.ts @@ -1,7 +1,7 @@ import { Server } from 'socket.io'; import { Server as HTTPServer } from 'http'; import corsOptions from '@base/config/cors'; -import registerChatHandlers from '@base/sockets/chat'; +import registerChatHandlers from '@base/sockets/chats'; import redisClient from '@base/config/redis'; import { Types } from 'mongoose'; import { joinAllRooms } from './MessagingServices'; diff --git a/src/types/normalChat.ts b/src/types/normalChat.ts index d1de112..aaa9b91 100644 --- a/src/types/normalChat.ts +++ b/src/types/normalChat.ts @@ -1,6 +1,9 @@ import IChat from './chat'; interface INormalChat extends IChat { + encryptionKey: String; + initializationVector: String; + authTag: String; destructionTimestamp: Date | undefined; destructionDuration: number | undefined; } diff --git a/src/utils/encryption.ts b/src/utils/encryption.ts new file mode 100644 index 0000000..b8e61bc --- /dev/null +++ b/src/utils/encryption.ts @@ -0,0 +1,29 @@ +import crypto from 'crypto'; + +export const encryptKey = (key: String, iv: String) => { + const cipher = crypto.createCipheriv( + 'aes-256-gcm', + Buffer.from(process.env.ENCRYPTION_KEY_SECRET as string, 'hex'), + Buffer.from(iv, 'hex') + ); + + const encryptedKey = Buffer.concat([ + cipher.update(Buffer.from(key, 'hex')), + cipher.final(), + ]).toString('hex'); + return { encryptedKey, authTag: cipher.getAuthTag().toString('hex') }; +}; + +export const decryptKey = (key: String, iv: String, authTag: String) => { + const decipher = crypto.createDecipheriv( + 'aes-256-gcm', + Buffer.from(process.env.ENCRYPTION_KEY_SECRET as string, 'hex'), + Buffer.from(iv, 'hex') + ); + decipher.setAuthTag(Buffer.from(authTag, 'hex')); + + return Buffer.concat([ + decipher.update(Buffer.from(key, 'hex')), + decipher.final(), + ]).toString('hex'); +}; From e1d23385e1936025ceab4ca85ef14f65a6dff09e Mon Sep 17 00:00:00 2001 From: AhmedHamed3699 Date: Tue, 17 Dec 2024 18:59:43 +0200 Subject: [PATCH 19/54] feat(chats): seed messages are now encrypted --- .env.example | 3 ++- docs/api/chat.swagger.ts | 33 ++++++++++++--------------------- src/database/seed/userSeed.ts | 13 +++++++++++-- src/models/chatModel.ts | 8 +++++--- src/models/normalChatModel.ts | 18 +++++++++++++----- src/types/normalChat.ts | 3 ++- src/utils/encryption.ts | 25 +++++++++++++++++++------ 7 files changed, 64 insertions(+), 39 deletions(-) diff --git a/.env.example b/.env.example index 866168b..76a7ba4 100644 --- a/.env.example +++ b/.env.example @@ -11,7 +11,8 @@ REDIS_LOCALHOST_URL=redis://localhost:6379 SERVER_URL=http://localhost:3000 WEBSOCKET_URL=ws://localhost:3000 -ENCRYPTION_KEY_SECRET=encryption-key-secret +ENCRYPTION_KEY_SECRET=encryption-key-secret #(32 bytes) +ENCRYPTION_KEY_IV=encryption-key-iv #(12 bytes) SESSION_SECRET=session-secret SESSION_EXPIRES_IN=180d diff --git a/docs/api/chat.swagger.ts b/docs/api/chat.swagger.ts index 4feae81..d397fd8 100644 --- a/docs/api/chat.swagger.ts +++ b/docs/api/chat.swagger.ts @@ -61,42 +61,30 @@ * Role: * type: string * description: Role of the user in the chat (e.g., member, admin). - * _id: - * type: string - * description: Unique identifier for the membership entry. - * id: - * type: string - * description: Alias for `_id`. * type: * type: string * description: Type of the chat (e.g., "private", "group"). + * encryptionKey: + * type: string + * description: Key used for encrypting chat messages. + * initializationVector: + * type: string + * description: Initialization vector for encrypting chat messages. * isDeleted: * type: boolean * description: Indicates if the chat has been deleted. * chatType: * type: string * description: Specific type of the chat (e.g., "NormalChat"). - * __v: - * type: integer - * description: Version key. * numberOfMembers: * type: integer * description: Total number of members in the chat. - * id: - * type: string - * description: Alias for `_id`. * isMuted: * type: boolean * description: Indicates if the chat is muted. * draft: * type: string * description: Draft message saved for the chat. - * _id: - * type: string - * description: Unique identifier for the chat entry. - * id: - * type: string - * description: Alias for `_id`. * members: * type: array * items: @@ -136,9 +124,6 @@ * items: * type: string * description: IDs of users blocked by this member. - * id: - * type: string - * description: Alias for `_id`. * lastMessages: * type: array * items: @@ -363,6 +348,12 @@ * contentType: * type: string * example: text + * media: + * type: string + * example: "media-file-name.jpg" + * isEdited: + * type: boolean + * example: false * isPinned: * type: boolean * example: false diff --git a/src/database/seed/userSeed.ts b/src/database/seed/userSeed.ts index b05f93d..17a0e16 100644 --- a/src/database/seed/userSeed.ts +++ b/src/database/seed/userSeed.ts @@ -2,8 +2,9 @@ import fs from 'fs'; import { faker } from '@faker-js/faker'; import User from '@models/userModel'; import Message from '@models/messageModel'; -import GroupChannel from '@base/models/groupChannelModel'; -import NormalChat from '@base/models/normalChatModel'; +import GroupChannel from '@models/groupChannelModel'; +import NormalChat from '@models/normalChatModel'; +import { decryptKey, encryptMessage } from '@utils/encryption'; const existingUsers = JSON.parse( fs.readFileSync(`${__dirname}/json/users.json`, 'utf-8') @@ -66,6 +67,14 @@ const createRandomMessage = async (chat: any) => { timestamp: faker.date.recent({ days: 30 }), }; + if (chat.encryptionKey) { + message.content = encryptMessage( + message.content, + decryptKey(chat.encryptionKey, chat.keyAuthTag), + decryptKey(chat.initializationVector, chat.vectorAuthTag) + ); + } + return Message.create(message); }; diff --git a/src/models/chatModel.ts b/src/models/chatModel.ts index 78ea4bf..5adc6b0 100644 --- a/src/models/chatModel.ts +++ b/src/models/chatModel.ts @@ -43,11 +43,13 @@ const chatSchema = new mongoose.Schema( }); } if (ret.encryptionKey) { - ret.encryptionKey = decryptKey( - ret.encryptionKey, + ret.encryptionKey = decryptKey(ret.encryptionKey, ret.keyAuthTag); + ret.initializationVector = decryptKey( ret.initializationVector, - ret.authTag + ret.vectorAuthTag ); + delete ret.keyAuthTag; + delete ret.vectorAuthTag; } return ret; }, diff --git a/src/models/normalChatModel.ts b/src/models/normalChatModel.ts index 0ca9036..c6e762f 100644 --- a/src/models/normalChatModel.ts +++ b/src/models/normalChatModel.ts @@ -11,9 +11,13 @@ const normalChatSchema = new mongoose.Schema({ }, initializationVector: { type: String, - default: crypto.randomBytes(12).toString('hex'), + default: crypto.randomBytes(16).toString('hex'), }, - authTag: { + keyAuthTag: { + type: String, + default: '', + }, + vectorAuthTag: { type: String, default: '', }, @@ -23,12 +27,16 @@ const normalChatSchema = new mongoose.Schema({ normalChatSchema.pre('save', function (next) { if (!this.isNew) return next(); - const { encryptedKey, authTag } = encryptKey( - this.encryptionKey, + const { encrypted: encryptedKey, authTag: keyAuthTag } = encryptKey( + this.encryptionKey + ); + const { encrypted: encryptedVector, authTag: vectorAuthTag } = encryptKey( this.initializationVector ); this.encryptionKey = encryptedKey; - this.authTag = authTag; + this.keyAuthTag = keyAuthTag; + this.initializationVector = encryptedVector; + this.vectorAuthTag = vectorAuthTag; next(); }); diff --git a/src/types/normalChat.ts b/src/types/normalChat.ts index aaa9b91..66b0955 100644 --- a/src/types/normalChat.ts +++ b/src/types/normalChat.ts @@ -3,7 +3,8 @@ import IChat from './chat'; interface INormalChat extends IChat { encryptionKey: String; initializationVector: String; - authTag: String; + keyAuthTag: String; + vectorAuthTag: String; destructionTimestamp: Date | undefined; destructionDuration: number | undefined; } diff --git a/src/utils/encryption.ts b/src/utils/encryption.ts index b8e61bc..bef0abd 100644 --- a/src/utils/encryption.ts +++ b/src/utils/encryption.ts @@ -1,24 +1,24 @@ import crypto from 'crypto'; -export const encryptKey = (key: String, iv: String) => { +export const encryptKey = (key: String) => { const cipher = crypto.createCipheriv( 'aes-256-gcm', Buffer.from(process.env.ENCRYPTION_KEY_SECRET as string, 'hex'), - Buffer.from(iv, 'hex') + Buffer.from(process.env.ENCRYPTION_KEY_IV as string, 'hex') ); - const encryptedKey = Buffer.concat([ + const encrypted = Buffer.concat([ cipher.update(Buffer.from(key, 'hex')), cipher.final(), ]).toString('hex'); - return { encryptedKey, authTag: cipher.getAuthTag().toString('hex') }; + return { encrypted, authTag: cipher.getAuthTag().toString('hex') }; }; -export const decryptKey = (key: String, iv: String, authTag: String) => { +export const decryptKey = (key: String, authTag: String) => { const decipher = crypto.createDecipheriv( 'aes-256-gcm', Buffer.from(process.env.ENCRYPTION_KEY_SECRET as string, 'hex'), - Buffer.from(iv, 'hex') + Buffer.from(process.env.ENCRYPTION_KEY_IV as string, 'hex') ); decipher.setAuthTag(Buffer.from(authTag, 'hex')); @@ -27,3 +27,16 @@ export const decryptKey = (key: String, iv: String, authTag: String) => { decipher.final(), ]).toString('hex'); }; + +export const encryptMessage = (message: String, key: String, iv: String) => { + const cipher = crypto.createCipheriv( + 'aes-256-cbc', + Buffer.from(key, 'hex'), + Buffer.from(iv, 'hex') + ); + + return Buffer.concat([ + cipher.update(Buffer.from(message)), + cipher.final(), + ]).toString('hex'); +}; From 401a1e3e66b10f6b220042cefc24f7daf2fb93ec Mon Sep 17 00:00:00 2001 From: Peter Safwat <118059454+PeterSafwatHBK@users.noreply.github.com> Date: Tue, 17 Dec 2024 19:24:02 +0200 Subject: [PATCH 20/54] fix(search): change search request to be POST (#141) * Update searchController.ts * search-post-to-get * search-get-to-post * search-get-to-post --- docs/api/search.swagger.ts | 4 ++-- src/routes/searchRoute.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/api/search.swagger.ts b/docs/api/search.swagger.ts index 363159d..331ec89 100644 --- a/docs/api/search.swagger.ts +++ b/docs/api/search.swagger.ts @@ -4,11 +4,10 @@ * name: Search * description: API for searching content across various spaces with optional filters and global search capabilities. */ - /** * @swagger * /search-request: - * get: + * post: * summary: Perform a search across specified spaces with optional filters. * tags: [Search] * description: Allows users to search within specific spaces like chats, channels, and groups, apply filters, and enable global search for a broader scope. @@ -155,6 +154,7 @@ * example: "An unexpected error occurred while processing the request." */ + /** * @swagger * tags: diff --git a/src/routes/searchRoute.ts b/src/routes/searchRoute.ts index bdbbdd6..b039f47 100644 --- a/src/routes/searchRoute.ts +++ b/src/routes/searchRoute.ts @@ -7,6 +7,6 @@ import restrictTo from '@base/middlewares/chatMiddlewares'; const router = Router(); router.use(protect); -router.get('/search-request',searchMessages); +router.post('/search-request',searchMessages); export default router; From cdd86946611141d0bf290b8ff5cef324c14b356e Mon Sep 17 00:00:00 2001 From: Amir Kedis <88613195+amir-kedis@users.noreply.github.com> Date: Tue, 17 Dec 2024 22:14:21 +0200 Subject: [PATCH 21/54] feat: add static analysis with sonar cube --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index b606303..d764491 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,12 @@ # telware backend + +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=TelwareSW_telware-backend&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=TelwareSW_telware-backend) +[![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=TelwareSW_telware-backend&metric=ncloc)](https://sonarcloud.io/summary/new_code?id=TelwareSW_telware-backend) +[![Technical Debt](https://sonarcloud.io/api/project_badges/measure?project=TelwareSW_telware-backend&metric=sqale_index)](https://sonarcloud.io/summary/new_code?id=TelwareSW_telware-backend) +[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=TelwareSW_telware-backend&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=TelwareSW_telware-backend) +[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=TelwareSW_telware-backend&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=TelwareSW_telware-backend) +[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=TelwareSW_telware-backend&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=TelwareSW_telware-backend) + + Backend Repo for TelWare Messaging Platform From c2835da18b1ac9ad2977d8439786604e6b96ee64 Mon Sep 17 00:00:00 2001 From: Peter Safwat <118059454+PeterSafwatHBK@users.noreply.github.com> Date: Tue, 17 Dec 2024 21:47:38 +0200 Subject: [PATCH 22/54] improve-search --- docs/api/search.swagger.ts | 2 +- src/controllers/searchController.ts | 107 +++++++++++++++++++++------- 2 files changed, 82 insertions(+), 27 deletions(-) diff --git a/docs/api/search.swagger.ts b/docs/api/search.swagger.ts index 331ec89..53c0e8c 100644 --- a/docs/api/search.swagger.ts +++ b/docs/api/search.swagger.ts @@ -6,7 +6,7 @@ */ /** * @swagger - * /search-request: + * /search/search-request: * post: * summary: Perform a search across specified spaces with optional filters. * tags: [Search] diff --git a/src/controllers/searchController.ts b/src/controllers/searchController.ts index 736baac..f1fe7b2 100644 --- a/src/controllers/searchController.ts +++ b/src/controllers/searchController.ts @@ -2,55 +2,111 @@ import { Response ,NextFunction } from 'express'; import Message from '@base/models/messageModel'; import Chat from '@base/models/chatModel'; import catchAsync from '@utils/catchAsync'; - - +import GroupChannel from '@base/models/groupChannelModel'; +import User from '@base/models/userModel'; +import IUser from '@base/types/user'; +import IChat from '@base/types/chat'; +import IGroupChannel from '@base/types/groupChannel'; export const searchMessages = catchAsync(async (req: any, res: Response, next: NextFunction) => { try { + let globalSearchResult: { + groups: IGroupChannel[]; + chats: IChat[]; + users: IUser[]; + } = { + groups: [], + chats: [], + users: [] + }; + const { query, searchSpace, filter, isGlobalSearch } = req.body; + // Input validation if (!query || !searchSpace || typeof isGlobalSearch === 'undefined') { return res.status(400).json({ message: 'Query, searchSpace, and isGlobalSearch are required' }); } - const searchConditions: any = { - content: { $regex: query as string, $options: 'i' }, - }; + const searchConditions: any = { content: { $regex: query, $options: 'i' } }; + // Handle contentType filter if (filter) { - const filterTypes = (filter as string).split(','); + const filterTypes = filter.split(','); searchConditions.contentType = { $in: filterTypes }; } - const spaces = (searchSpace as string).split(','); - const messageTypes: string[] = []; - - if (spaces.includes('chats')) searchConditions.chatId = { $exists: true }; - if (spaces.includes('channels')) messageTypes.push('channel'); - if (spaces.includes('groups')) messageTypes.push('group'); + // Prepare chat type filters + const spaces = searchSpace.split(','); + const chatTypeConditions: any[] = []; - if (messageTypes.length > 0) { - searchConditions.messageType = { $in: messageTypes }; + if (spaces.includes('chats')) { + chatTypeConditions.push({ type: 'private' }); + } + if (spaces.includes('channels')) { + chatTypeConditions.push({ type: 'channel' }); + } + if (spaces.includes('groups')) { + chatTypeConditions.push({ type: 'group' }); } + // Limit search to user's chats unless global search + let chatFilter: any = {}; const userChats = await Chat.find({ - members: { $elemMatch: { _id: req.user._id } }, - }).select('_id'); + members: { $elemMatch: { user: req.user._id } }, + }).select('_id type'); + + // Filter user chats by type + const filteredChats = userChats.filter((chat) => + chatTypeConditions.length > 0 ? chatTypeConditions.some((cond) => cond.type === chat.type) : true + ); - console.log(userChats); - const chatIds = userChats.map((chat) => chat._id); - searchConditions.chatId = { $in: chatIds }; + const chatIds = filteredChats.map((chat) => chat._id); + chatFilter = { chatId: { $in: chatIds } }; - console.log(searchConditions); + // Combine filters + const finalSearchConditions = { ...searchConditions, ...chatFilter }; - const messages = await Message.find(searchConditions) + // Fetch messages and populate references + const messages = await Message.find(finalSearchConditions) .populate('senderId', 'username') - .populate('chatId', 'name') - .limit(50) - .exec(); + .populate({ + path: 'chatId', + select: 'name type', + }) + .limit(50); + + // Global Search for Groups, Channels, and Chats + if (isGlobalSearch) { + // Groups and Channels by name + const groupsAndChannels = await GroupChannel.find({ + name: { $regex: query, $options: 'i' }, + }).select('name type picture'); + + globalSearchResult.groups = groupsAndChannels.filter((gc: IGroupChannel) => gc.type === 'group'); + globalSearchResult.chats = groupsAndChannels.filter((gc: IGroupChannel) => gc.type === 'channel'); + + // Users (to find chats involving usernames) + const users = await User.find({ + username: { $regex: query, $options: 'i' }, + }).select('username _id'); + + globalSearchResult.users = users; + + // Chats where the user is a member and the username matches + const userIds = users.map((user) => user._id); + const chats = await Chat.find({ + members: { $elemMatch: { user: { $in: userIds } } }, + type: 'private', + }).select('type members'); + + globalSearchResult.chats.push(...chats); + } res.status(200).json({ success: true, - data: messages, + data: { + searchResult: messages, + globalSearchResult: globalSearchResult, + }, }); } catch (error) { console.error('Error in searchMessages:', error); @@ -58,7 +114,6 @@ export const searchMessages = catchAsync(async (req: any, res: Response, next: N } }); - export const searchMessagesDummmy = catchAsync(async (req: any, res: Response, next: NextFunction) => { try { const userId = req.user.id; From b57680bf9f6a96729fb0e0e8f58413e5083a59e0 Mon Sep 17 00:00:00 2001 From: Peter Safwat <118059454+PeterSafwatHBK@users.noreply.github.com> Date: Tue, 17 Dec 2024 23:02:50 +0200 Subject: [PATCH 23/54] improve-search-fix --- src/controllers/searchController.ts | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/controllers/searchController.ts b/src/controllers/searchController.ts index f1fe7b2..f5b4265 100644 --- a/src/controllers/searchController.ts +++ b/src/controllers/searchController.ts @@ -11,12 +11,12 @@ export const searchMessages = catchAsync(async (req: any, res: Response, next: N try { let globalSearchResult: { groups: IGroupChannel[]; - chats: IChat[]; users: IUser[]; + channels: IGroupChannel[]; } = { groups: [], - chats: [], - users: [] + users: [], + channels: [] }; const { query, searchSpace, filter, isGlobalSearch } = req.body; @@ -82,23 +82,28 @@ export const searchMessages = catchAsync(async (req: any, res: Response, next: N }).select('name type picture'); globalSearchResult.groups = groupsAndChannels.filter((gc: IGroupChannel) => gc.type === 'group'); - globalSearchResult.chats = groupsAndChannels.filter((gc: IGroupChannel) => gc.type === 'channel'); + globalSearchResult.channels = groupsAndChannels.filter((gc: IGroupChannel) => gc.type === 'channel'); // Users (to find chats involving usernames) const users = await User.find({ - username: { $regex: query, $options: 'i' }, - }).select('username _id'); + $or: [ + { screenFirstName: { $regex: query, $options: 'i' } }, + { screenLastName: { $regex: query, $options: 'i' } }, + { username: { $regex: query, $options: 'i' } } + ] + }).select('name username _id'); + globalSearchResult.users = users; // Chats where the user is a member and the username matches - const userIds = users.map((user) => user._id); - const chats = await Chat.find({ - members: { $elemMatch: { user: { $in: userIds } } }, - type: 'private', - }).select('type members'); + // const userIds = users.map((user) => user._id); + // const chats = await Chat.find({ + // members: { $elemMatch: { user: { $in: userIds } } }, + // type: 'private', + // }).select('type members'); - globalSearchResult.chats.push(...chats); + // globalSearchResult.chats.push(...chats); } res.status(200).json({ From fc155d53c3d905c686944058e33236f31fe45e51 Mon Sep 17 00:00:00 2001 From: AhmedHamed3699 Date: Tue, 17 Dec 2024 23:38:19 +0200 Subject: [PATCH 24/54] feat(messages): deliver messages on receive --- src/models/messageModel.ts | 14 ++++++++++++++ src/sockets/messages.ts | 13 ++++++++++--- src/types/message.ts | 5 ++--- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/models/messageModel.ts b/src/models/messageModel.ts index f628205..d1da292 100644 --- a/src/models/messageModel.ts +++ b/src/models/messageModel.ts @@ -26,6 +26,20 @@ const messageSchema = new mongoose.Schema({ type: Boolean, default: false, }, + deliveredTo: [ + { + type: mongoose.Types.ObjectId, + ref: 'User', + default: [], + }, + ], + readBy: [ + { + type: mongoose.Types.ObjectId, + ref: 'User', + default: [], + }, + ], parentMessageId: mongoose.Types.ObjectId, threadMessages: [ { diff --git a/src/sockets/messages.ts b/src/sockets/messages.ts index 067432b..6f63e28 100644 --- a/src/sockets/messages.ts +++ b/src/sockets/messages.ts @@ -4,7 +4,7 @@ import IMessage from '@base/types/message'; import Message from '@models/messageModel'; import { enableDestruction } from '@services/chatService'; import Chat from '@base/models/chatModel'; -import { check, updateDraft } from './MessagingServices'; +import { check, informSessions, updateDraft } from './MessagingServices'; interface PinUnPinMessageData { chatId: string | Types.ObjectId; @@ -77,7 +77,14 @@ export const handleMessaging = async ( } await updateDraft(io, senderId, chatId, ''); - socket.to(chatId).emit('RECEIVE_MESSAGE', message); + socket.to(chatId).emit('RECEIVE_MESSAGE', message, async (res: any) => { + if (res.success) { + if (res.isRead) message.readBy.push(res.userId); + else message.deliveredTo.push(res.userId); + await message.save(); + informSessions(io, senderId, message, 'MESSAGE_DELIVERED'); + } + }); const res = { messageId: message._id, }; @@ -141,7 +148,7 @@ export const handleDeleteMessage = async ( message: 'Failed to delete the message', error: 'no message found with the provided id', }); - socket.to(chatId).emit('DELETE_MESSAGE_SERVER', messageId); + socket.to(chatId).emit('DELETE_MESSAGE_SERVER', message); ack({ success: true, message: 'Message deleted successfully' }); }; diff --git a/src/types/message.ts b/src/types/message.ts index 9c4b707..359aa62 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -2,7 +2,6 @@ import { Types } from 'mongoose'; import ICommunication from './communication'; interface IMessage extends ICommunication { - timestamp: Date; media: string; content: string; contentType: string; @@ -10,8 +9,8 @@ interface IMessage extends ICommunication { isForward: boolean; isEdited: boolean; isAnnouncement: boolean; - senderId: Types.ObjectId; - chatId: Types.ObjectId; + deliveredTo: Types.ObjectId[]; + readBy: Types.ObjectId[]; parentMessageId: Types.ObjectId | undefined; threadMessages: Types.ObjectId[]; } From 527bf2c08640edd3cf566c3b9de585e5b9fc55f5 Mon Sep 17 00:00:00 2001 From: AhmedHamed3699 Date: Wed, 18 Dec 2024 04:35:11 +0200 Subject: [PATCH 25/54] feat(messages): add read message functionality --- src/controllers/chatController.ts | 17 ++-- src/models/userModel.ts | 10 +-- src/services/chatService.ts | 22 +++++- src/sockets/MessagingServices.ts | 53 ++++++++++--- src/sockets/messages.ts | 125 +++++++++++++++++++----------- src/sockets/socket.ts | 9 ++- src/types/user.ts | 1 - 7 files changed, 159 insertions(+), 78 deletions(-) diff --git a/src/controllers/chatController.ts b/src/controllers/chatController.ts index 5b3a6cb..3c2bfe9 100644 --- a/src/controllers/chatController.ts +++ b/src/controllers/chatController.ts @@ -8,6 +8,7 @@ import { getLastMessage, unmute, deleteChatPictureFile, + getUnreadMessages, } from '@base/services/chatService'; import IUser from '@base/types/user'; import catchAsync from '@base/utils/catchAsync'; @@ -39,16 +40,20 @@ export const getAllChats = catchAsync( ) ), ]; - const members = await User.find( - { _id: { $in: memberIds } }, - 'username screenFirstName screenLastName phoneNumber photo status isAdmin stories blockedUsers' - ); - const lastMessages = await getLastMessage(allChats); + + const [members, lastMessages, unreadMessages] = await Promise.all([ + User.find( + { _id: { $in: memberIds } }, + 'username screenFirstName screenLastName phoneNumber photo status isAdmin stories blockedUsers' + ), + getLastMessage(allChats), + getUnreadMessages(allChats, userId), + ]); res.status(200).json({ status: 'success', message: 'Chats retrieved successfuly', - data: { chats: allChats, members, lastMessages }, + data: { chats: allChats, members, lastMessages, unreadMessages }, }); } ); diff --git a/src/models/userModel.ts b/src/models/userModel.ts index 24be644..8e0a223 100644 --- a/src/models/userModel.ts +++ b/src/models/userModel.ts @@ -219,6 +219,7 @@ const userSchema = new mongoose.Schema( virtuals: true, transform(doc, ret) { delete ret.__v; + console.log(ret); if (ret.chats) { ret.chats.forEach((chat: any) => { delete chat.id; @@ -233,8 +234,6 @@ const userSchema = new mongoose.Schema( } ); -//TODO: unreadMessages virtual property - userSchema.index({ email: 1 }, { background: true }); userSchema.pre('save', async function (next) { @@ -268,13 +267,6 @@ userSchema.methods.passwordChanged = function (tokenIssuedAt: number): boolean { return false; }; -//FIX: fix this function -userSchema.methods.selectFields = function (): void { - this.select( - '-__v -provider -providerId -password -isAdmin -stories -blockedUsers -contacts -chats -changedPasswordAt -emailVerificationCode -emailVerificationCodeExpires -resetPasswordToken -resetPasswordExpires' - ); -}; - userSchema.methods.generateSaveConfirmationCode = function (): string { const confirmationCode: string = generateConfirmationCode(); this.emailVerificationCode = crypto diff --git a/src/services/chatService.ts b/src/services/chatService.ts index ec590bb..2e782c2 100644 --- a/src/services/chatService.ts +++ b/src/services/chatService.ts @@ -11,9 +11,11 @@ import deleteFile from '@base/utils/deleteFile'; export const getLastMessage = async (chats: any) => { const lastMessages = await Promise.all( chats.map(async (chat: any) => { - const lastMessage = await Message.findOne({ chatId: chat.chat }).sort({ - timestamp: -1, - }); + const lastMessage = await Message.findOne({ chatId: chat.chat._id }).sort( + { + timestamp: -1, + } + ); return { chatId: chat.chat._id, lastMessage, @@ -23,6 +25,20 @@ export const getLastMessage = async (chats: any) => { return lastMessages; }; +export const getUnreadMessages = async ( + chats: any, + userId: mongoose.Types.ObjectId +) => + Promise.all( + chats.map(async (chat: any) => ({ + chatId: chat.chat._id, + unreadMessagesCount: await Message.countDocuments({ + chatId: chat.chat._id, + readBy: { $nin: [userId] }, + }), + })) + ); + export const getChats = async ( userId: mongoose.Types.ObjectId, type?: string diff --git a/src/sockets/MessagingServices.ts b/src/sockets/MessagingServices.ts index 4afc017..7c07ba5 100644 --- a/src/sockets/MessagingServices.ts +++ b/src/sockets/MessagingServices.ts @@ -1,9 +1,11 @@ -import { Socket } from 'socket.io'; -import { getChatIds } from '@services/chatService'; +import { Server, Socket } from 'socket.io'; import { Types } from 'mongoose'; -import { getSocketsByUserId } from '@base/services/sessionService'; -import User from '@base/models/userModel'; -import GroupChannel from '@base/models/groupChannelModel'; +import { getChatIds } from '@services/chatService'; +import { getSocketsByUserId } from '@services/sessionService'; +import User from '@models/userModel'; +import GroupChannel from '@models/groupChannelModel'; +import Message from '@models/messageModel'; +import IMessage from '@base/types/message'; export interface Member { user: Types.ObjectId; @@ -71,7 +73,7 @@ export const check = async ( }; export const informSessions = async ( - io: any, + io: Server, userId: string, data: any, event: string @@ -86,31 +88,31 @@ export const informSessions = async ( }; export const joinRoom = async ( - io: any, + io: Server, roomId: String, userId: Types.ObjectId ) => { const socketIds = await getSocketsByUserId(userId); socketIds.forEach((socketId: string) => { const socket = io.sockets.sockets.get(socketId); - if (socket) socket.join(roomId); + if (socket) socket.join(roomId.toString()); }); }; export const updateDraft = async ( - io: any, + io: Server, senderId: string, chatId: string, content: string ) => { - await User.findByIdAndUpdate( + User.findByIdAndUpdate( senderId, { $set: { 'chats.$[chat].draft': content } }, { arrayFilters: [{ 'chat.chat': chatId }], } ); - await informSessions( + informSessions( io, senderId, { chatId, draft: content }, @@ -124,3 +126,32 @@ export const joinAllRooms = async (socket: Socket, userId: Types.ObjectId) => { socket.join(chatId.toString()); }); }; + +export const deliverMessages = async ( + io: Server, + socket: Socket, + userId: Types.ObjectId +) => { + const user = await User.findById(userId); + if (!user) return; + + const messages = await Message.find({ + chatId: { $in: user.chats.map((chat: any) => chat.chat) }, + senderId: { $ne: userId }, + deliveredTo: { $nin: [userId] }, + readBy: { $nin: [userId] }, + }); + + Promise.all( + messages.map(async (message: IMessage) => { + message.deliveredTo.push(userId); + message.save(); + informSessions( + io, + message.senderId.toString(), + message, + 'MESSAGE_DELIVERED' + ); + }) + ); +}; diff --git a/src/sockets/messages.ts b/src/sockets/messages.ts index 6f63e28..7778128 100644 --- a/src/sockets/messages.ts +++ b/src/sockets/messages.ts @@ -11,7 +11,7 @@ interface PinUnPinMessageData { messageId: string | Types.ObjectId; } -export const handleMessaging = async ( +const handleMessaging = async ( io: any, socket: Socket, data: any, @@ -81,8 +81,13 @@ export const handleMessaging = async ( if (res.success) { if (res.isRead) message.readBy.push(res.userId); else message.deliveredTo.push(res.userId); - await message.save(); - informSessions(io, senderId, message, 'MESSAGE_DELIVERED'); + message.save(); + informSessions( + io, + senderId, + message, + res.isRead ? 'MESSAGE_READ_SERVER' : 'MESSAGE_DELIVERED' + ); } }); const res = { @@ -92,11 +97,7 @@ export const handleMessaging = async ( ack({ success: true, message: 'Message sent successfully', res }); }; -export const handleEditMessage = async ( - socket: Socket, - data: any, - ack: Function -) => { +const handleEditMessage = async (socket: Socket, data: any, ack: Function) => { const { messageId, content, chatId } = data; if (!messageId || !content) return ack({ @@ -129,7 +130,7 @@ export const handleEditMessage = async ( }); }; -export const handleDeleteMessage = async ( +const handleDeleteMessage = async ( socket: Socket, data: any, ack: Function @@ -152,43 +153,73 @@ export const handleDeleteMessage = async ( ack({ success: true, message: 'Message deleted successfully' }); }; -async function handlePinMessage(socket: Socket, data: PinUnPinMessageData) { - try { - // Make a message pinned - const message = await Message.findById(data.messageId); - if (!message) { - //TODO: Make a global socket event for the client to send errors to - return; - } - - message.isPinned = true; - await message.save(); +const handleReadMessage = async ( + io: Server, + socket: Socket, + data: any, + ack: Function, + userId: string +) => { + const { chatId } = data; + const messages = await Message.find({ + chatId, + senderId: { $ne: userId }, + readBy: { $nin: [userId] }, + }); + if (!messages) + return ack({ + success: true, + message: 'No messages to read', + }); + messages.forEach(async (message: IMessage) => { + message.deliveredTo = message.deliveredTo.filter( + (id) => id.toString() !== userId + ); + message.readBy.push(new Types.ObjectId(userId)); + message.save(); + informSessions( + io, + message.senderId.toString(), + message, + 'MESSAGE_READ_SERVER' + ); + }); + ack({ success: true, message: 'Message read successfully' }); +}; - // Send an event to all online chat users to pin a message. - socket.to(data.chatId.toString()).emit('PIN_MESSAGE_SERVER', data); - } catch (err) { - //TODO: Make a global socket event for the client to send errors to +const handlePinMessage = async ( + socket: Socket, + data: PinUnPinMessageData, + ack: Function +) => { + const message = await Message.findById(data.messageId); + if (!message) { + return ack({ success: false, message: 'Failed to pin message' }); } -} -async function handleUnPinMessage(socket: Socket, data: PinUnPinMessageData) { - try { - // Make a message unpinned - const message = await Message.findById(data.messageId); - if (!message) { - //TODO: Make a global socket event for the client to send errors to - return; - } + message.isPinned = true; + await message.save(); - message.isPinned = false; - await message.save(); + socket.to(data.chatId.toString()).emit('PIN_MESSAGE_SERVER', data); + ack({ success: true, message: 'Message pinned successfully' }); +}; - // Send an event to all online chat users to unpin a message. - socket.to(data.chatId.toString()).emit('UNPIN_MESSAGE_SERVER', data); - } catch (err) { - //TODO: Make a global socket event for the client to send errors to +const handleUnPinMessage = async ( + socket: Socket, + data: PinUnPinMessageData, + ack: Function +) => { + const message = await Message.findById(data.messageId); + if (!message) { + return ack({ success: false, message: 'Failed to unpin message' }); } -} + + message.isPinned = false; + await message.save(); + + socket.to(data.chatId.toString()).emit('UNPIN_MESSAGE_SERVER', data); + ack({ success: true, message: 'Message unpinned successfully' }); +}; async function registerMessagesHandlers( io: Server, @@ -207,12 +238,18 @@ async function registerMessagesHandlers( handleDeleteMessage(socket, data, ack) ); - socket.on('PIN_MESSAGE_CLIENT', (data: PinUnPinMessageData) => - handlePinMessage(socket, data) + socket.on('MESSAGE_READ_CLIENT', (data: any, ack: Function) => { + handleReadMessage(io, socket, data, ack, userId); + }); + + socket.on('PIN_MESSAGE_CLIENT', (data: PinUnPinMessageData, ack: Function) => + handlePinMessage(socket, data, ack) ); - socket.on('UNPIN_MESSAGE_CLIENT', (data: PinUnPinMessageData) => - handleUnPinMessage(socket, data) + socket.on( + 'UNPIN_MESSAGE_CLIENT', + (data: PinUnPinMessageData, ack: Function) => + handleUnPinMessage(socket, data, ack) ); } diff --git a/src/sockets/socket.ts b/src/sockets/socket.ts index 43d5724..3c9f06d 100644 --- a/src/sockets/socket.ts +++ b/src/sockets/socket.ts @@ -4,7 +4,7 @@ import corsOptions from '@base/config/cors'; import registerChatHandlers from '@base/sockets/chats'; import redisClient from '@base/config/redis'; import { Types } from 'mongoose'; -import { joinAllRooms } from './MessagingServices'; +import { deliverMessages, joinAllRooms } from './MessagingServices'; import registerMessagesHandlers from './messages'; import { authorizeSocket, protectSocket } from './middlewares'; import registerVoiceCallHandlers from './voiceCalls'; @@ -18,9 +18,10 @@ const socketSetup = (server: HTTPServer) => { io.use(protectSocket); io.on('connection', async (socket: any) => { - const userId = socket.request.session.user.id; + const userId = socket.request.session.user.id as string; console.log(`New client with userID ${userId} connected: ${socket.id}`); - await joinAllRooms(socket, new Types.ObjectId(userId as string)); + await joinAllRooms(socket, new Types.ObjectId(userId)); + deliverMessages(io, socket, new Types.ObjectId(userId)); socket.on('error', (error: Error) => { console.error(`Socket error on ${socket.id}:`, error); @@ -36,7 +37,7 @@ const socketSetup = (server: HTTPServer) => { socket.request.session.user.lastSeenTime = Date.now(); socket.request.session.user.status = 'offline'; socket.request.session.save(); - await redisClient.sRem(`user:${userId}:sockets`, socket.id); + redisClient.sRem(`user:${userId}:sockets`, socket.id); }); registerChatHandlers(io, socket, userId); diff --git a/src/types/user.ts b/src/types/user.ts index 5f02951..970d709 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -43,7 +43,6 @@ interface IUser extends Document { isCorrectPassword: (_candidatePass: string) => Promise; passwordChanged: (_tokenIssuedAt: number) => boolean; createResetPasswordToken: () => string; - selectFields: () => void; } export default IUser; From b869c05b71aed792768b471543a0f4c21a0dae1c Mon Sep 17 00:00:00 2001 From: AhmedHamed3699 Date: Wed, 18 Dec 2024 12:03:02 +0200 Subject: [PATCH 26/54] hotfix: receive media & documentation --- docs/api/sockets.swagger.ts | 51 +++++++++++++++++++++++++++++++++++++ src/app.ts | 2 +- src/routes/apiRoute.ts | 2 +- src/sockets/messages.ts | 1 + 4 files changed, 54 insertions(+), 2 deletions(-) diff --git a/docs/api/sockets.swagger.ts b/docs/api/sockets.swagger.ts index dd91af8..785f66b 100644 --- a/docs/api/sockets.swagger.ts +++ b/docs/api/sockets.swagger.ts @@ -315,6 +315,57 @@ * example: "no message found with the provided id" */ +/** + * @swagger + * HANDLE_READ_MESSAGE: + * post: + * summary: Marks messages as read + * description: Updates the status of unread messages in a chat for the user. Marks them as read and notifies the sender. + * tags: [Sockets] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * required: + * - chatId + * properties: + * chatId: + * type: string + * description: The unique ID of the chat whose messages need to be marked as read. + * responses: + * 200: + * description: Messages marked as read successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful. + * message: + * type: string + * description: A message describing the result. + * 400: + * description: Missing required fields or invalid input. + * content: + * application/json: + * schema: + * type: object + * properties: + * success: + * type: boolean + * description: Indicates if the operation was successful. + * message: + * type: string + * description: An error message describing the problem. + * error: + * type: string + * description: Details about the error (e.g., missing fields). + */ + /** * @swagger * UPDATE_DRAFT_CLIENT: diff --git a/src/app.ts b/src/app.ts index 016d5d8..e635a58 100644 --- a/src/app.ts +++ b/src/app.ts @@ -36,7 +36,7 @@ const swaggerOptions = { }, servers: [ { - url: `${process.env.SERVER_URL}/api/v1/`, + url: `${process.env.SERVER_URL}/api/v1`, description: 'HTTP server', }, { diff --git a/src/routes/apiRoute.ts b/src/routes/apiRoute.ts index 27dcd26..293d9e1 100644 --- a/src/routes/apiRoute.ts +++ b/src/routes/apiRoute.ts @@ -3,7 +3,7 @@ import authRouter from '@routes/authRoute'; import userRouter from '@routes/userRoute'; import storyRouter from '@base/routes/storyRoute'; import chatRouter from '@base/routes/chatRoute'; -import searchRouter from '@base/routes/searchRoute' +import searchRouter from '@base/routes/searchRoute'; const apiRouter = Router(); diff --git a/src/sockets/messages.ts b/src/sockets/messages.ts index 7778128..a842f33 100644 --- a/src/sockets/messages.ts +++ b/src/sockets/messages.ts @@ -63,6 +63,7 @@ const handleMessaging = async ( } const message = new Message({ + media, content, contentType, isForward, From 02e2b283fa556a2d1c75e20d177ae3d3effc5e21 Mon Sep 17 00:00:00 2001 From: Peter Safwat <118059454+PeterSafwatHBK@users.noreply.github.com> Date: Wed, 18 Dec 2024 14:52:21 +0200 Subject: [PATCH 27/54] feat(search): Search response updated --- src/controllers/searchController.ts | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/controllers/searchController.ts b/src/controllers/searchController.ts index f5b4265..42879e4 100644 --- a/src/controllers/searchController.ts +++ b/src/controllers/searchController.ts @@ -67,12 +67,16 @@ export const searchMessages = catchAsync(async (req: any, res: Response, next: N // Fetch messages and populate references const messages = await Message.find(finalSearchConditions) - .populate('senderId', 'username') - .populate({ - path: 'chatId', - select: 'name type', - }) - .limit(50); + .populate({ + path: 'senderId', + select: 'username screenFirstName screenLastName phoneNumber photo bio accountStatus stories', + }) + .populate({ + path: 'chatId', + select: 'name type', + }) + .limit(50); + // Global Search for Groups, Channels, and Chats if (isGlobalSearch) { @@ -91,7 +95,7 @@ export const searchMessages = catchAsync(async (req: any, res: Response, next: N { screenLastName: { $regex: query, $options: 'i' } }, { username: { $regex: query, $options: 'i' } } ] - }).select('name username _id'); + }).select('name username _id screenFirstName screenLastName phoneNumber photo bio accountStatus stories'); globalSearchResult.users = users; From 7aea725a37520af99b321aaa9a4d643d175b0e43 Mon Sep 17 00:00:00 2001 From: AhmedHamed3699 Date: Wed, 18 Dec 2024 17:03:53 +0200 Subject: [PATCH 28/54] hotfix(chats): solved add members bug --- src/models/userModel.ts | 1 - src/sockets/chats.ts | 72 ++++++++++++++++++++++------------------- 2 files changed, 39 insertions(+), 34 deletions(-) diff --git a/src/models/userModel.ts b/src/models/userModel.ts index 8e0a223..a797111 100644 --- a/src/models/userModel.ts +++ b/src/models/userModel.ts @@ -219,7 +219,6 @@ const userSchema = new mongoose.Schema( virtuals: true, transform(doc, ret) { delete ret.__v; - console.log(ret); if (ret.chats) { ret.chats.forEach((chat: any) => { delete chat.id; diff --git a/src/sockets/chats.ts b/src/sockets/chats.ts index b8308c0..951837a 100644 --- a/src/sockets/chats.ts +++ b/src/sockets/chats.ts @@ -19,7 +19,7 @@ const handleAddAdmins = async ( senderId: any ) => { const { members, chatId } = data; - const forbiddenUsers: string[] = []; + const InvalidUsers: string[] = []; const chat = await Chat.findById(chatId); const func = await check(chat, ack, senderId, { @@ -33,14 +33,14 @@ const handleAddAdmins = async ( const user = await User.findById(memId); if (!user) { - forbiddenUsers.push(memId); + InvalidUsers.push(memId); return; } const isMemberOfChat = chat?.members.some((m) => m.user.equals(memId)); if (!isMemberOfChat) { - forbiddenUsers.push(memId); + InvalidUsers.push(memId); return; } @@ -57,11 +57,11 @@ const handleAddAdmins = async ( }) ); - if (forbiddenUsers.length > 0) { + if (InvalidUsers.length > 0) { return ack({ success: false, message: 'Some users could not be added as admins', - error: `Could not add users with IDs: ${forbiddenUsers.join(', ')}`, + error: `Could not add users with IDs: ${InvalidUsers.join(', ')}`, }); } @@ -79,7 +79,7 @@ const handleAddMembers = async ( senderId: any ) => { const { chatId, users } = data; - const forbiddenUsers: string[] = []; + const invalidUsers: string[] = []; const chat = await Chat.findById(chatId); const func = await check(chat, ack, senderId, { @@ -88,45 +88,55 @@ const handleAddMembers = async ( }); if (func) return func; + if ( + chat?.type === 'group' && + chat.members.length + users.length > + parseInt(process.env.GROUP_SIZE ?? '10', 10) + ) + return ack({ + success: false, + message: 'Faild to create the chat', + error: `groups cannot have more than ${process.env.GROUP_SIZE ?? '10'} members`, + }); + await Promise.all( users.map(async (userId: any) => { const user = await User.findById(userId); if (!user) { - forbiddenUsers.push(userId); + invalidUsers.push(userId); return; } const isAlreadyMember = chat?.members.some((m: any) => m.user.equals(userId) ); - if (!isAlreadyMember) - chat?.members.push({ user: userId, Role: 'member' }); - const userWasMember = user.chats.some((c: any) => c.chat.equals(chatId)); - if (!userWasMember) user.chats.push(chatId); - console.log(user.chats); if (isAlreadyMember) { - forbiddenUsers.push(userId); + invalidUsers.push(userId); return; } - await informSessions(io, userId, { chatId }, 'ADD_MEMBERS_SERVER'); + chat?.members.push({ user: userId, Role: 'member' }); + const userWasMember = user.chats.some((c: any) => c.chat.equals(chatId)); + if (!userWasMember) + User.findByIdAndUpdate( + userId, + { $push: { chats: { chat: chatId } } }, + { new: true } + ); + + informSessions(io, userId, { chatId }, 'ADD_MEMBERS_SERVER'); }) ); - if (forbiddenUsers.length > 0) { - return ack({ - success: false, - message: 'Some users could not be added', - error: `Could not add users with IDs: ${forbiddenUsers.join(', ')}`, - }); - } - - await chat?.save(); + await chat?.save({ validateBeforeSave: false }); ack({ success: true, - message: 'Members added successfully', + message: + invalidUsers.length > 0 + ? `Some users could not be added, IDs: ${invalidUsers.join(', ')}` + : 'Members added successfully', data: {}, }); }; @@ -197,18 +207,14 @@ const handleCreateGroupChannel = async ( error: 'you need to login first', }); - if (!process.env.GROUP_SIZE) - return ack({ - success: false, - message: 'Faild to create the chat', - error: 'define GROUP_SIZE in your .env file', - }); - - if (type === 'group' && members.length > process.env.GROUP_SIZE) + if ( + type === 'group' && + members.length > parseInt(process.env.GROUP_SIZE ?? '10', 10) + ) return ack({ success: false, message: 'Faild to create the chat', - error: `groups cannot have more than ${process.env.GROUP_SIZE} members`, + error: `groups cannot have more than ${process.env.GROUP_SIZE ?? '10'} members`, }); const membersWithRoles = members.map((id: Types.ObjectId) => ({ From a2934491d3f09c9904ba1a2f13a02f339d7ec01a Mon Sep 17 00:00:00 2001 From: AhmedHamed3699 Date: Thu, 19 Dec 2024 23:05:51 +0200 Subject: [PATCH 29/54] fix(chats): fix small bug in creating groupt/channel --- src/sockets/MessagingServices.ts | 7 ++++--- src/sockets/chats.ts | 16 +++++++++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/sockets/MessagingServices.ts b/src/sockets/MessagingServices.ts index 7c07ba5..fd4bc6d 100644 --- a/src/sockets/MessagingServices.ts +++ b/src/sockets/MessagingServices.ts @@ -59,7 +59,7 @@ export const check = async ( if (sender.Role !== 'admin' && chat.type !== 'private') { const groupChannelChat = await GroupChannel.findById(chat._id); - if (!groupChannelChat.messagnigPermission) + if (!groupChannelChat.messagingPermission) return ack({ success: false, message: 'only admins can post and reply to this chat', @@ -70,6 +70,7 @@ export const check = async ( message: 'only admins can post to this channel', }); } + return true; }; export const informSessions = async ( @@ -93,7 +94,7 @@ export const joinRoom = async ( userId: Types.ObjectId ) => { const socketIds = await getSocketsByUserId(userId); - socketIds.forEach((socketId: string) => { + socketIds.forEach(async (socketId: string) => { const socket = io.sockets.sockets.get(socketId); if (socket) socket.join(roomId.toString()); }); @@ -122,7 +123,7 @@ export const updateDraft = async ( export const joinAllRooms = async (socket: Socket, userId: Types.ObjectId) => { const chatIds = await getChatIds(userId); - chatIds.forEach((chatId: Types.ObjectId) => { + chatIds.forEach(async (chatId: Types.ObjectId) => { socket.join(chatId.toString()); }); }; diff --git a/src/sockets/chats.ts b/src/sockets/chats.ts index 951837a..c8f3f11 100644 --- a/src/sockets/chats.ts +++ b/src/sockets/chats.ts @@ -14,6 +14,7 @@ import { const handleAddAdmins = async ( io: any, + socket: Socket, data: any, ack: Function, senderId: any @@ -44,7 +45,7 @@ const handleAddAdmins = async ( return; } - await Chat.findByIdAndUpdate( + Chat.findByIdAndUpdate( chatId, { $set: { 'members.$[elem].Role': 'admin' } }, { @@ -52,8 +53,7 @@ const handleAddAdmins = async ( arrayFilters: [{ 'elem.user': memId }], } ); - - await informSessions(io, memId, { chatId }, 'ADD_ADMINS_SERVER'); + socket.to(chatId).emit('ADD_ADMINS_SERVER', { chatId, memId }); }) ); @@ -74,6 +74,7 @@ const handleAddAdmins = async ( const handleAddMembers = async ( io: any, + socket: Socket, data: any, ack: Function, senderId: any @@ -125,7 +126,8 @@ const handleAddMembers = async ( { new: true } ); - informSessions(io, userId, { chatId }, 'ADD_MEMBERS_SERVER'); + await joinRoom(io, chatId as string, userId); + socket.to(chatId).emit('ADD_MEMBERS_SERVER', { chatId, userId }); }) ); @@ -235,7 +237,7 @@ const handleCreateGroupChannel = async ( }); await newChat.save(); await Promise.all([ - allMembers.map((member) => + allMembers.map(async (member) => joinRoom(io, newChat._id as string, member.user) ), User.updateMany( @@ -510,11 +512,11 @@ const registerChatHandlers = (io: Server, socket: Socket, userId: any) => { }); socket.on('ADD_ADMINS_CLIENT', (data: any, ack: Function) => { - handleAddAdmins(io, data, ack, userId); + handleAddAdmins(io, socket, data, ack, userId); }); socket.on('ADD_MEMBERS_CLIENT', (data: any, ack: Function) => { - handleAddMembers(io, data, ack, userId); + handleAddMembers(io, socket, data, ack, userId); }); socket.on('REMOVE_MEMBERS_CLIENT', (data: any, ack: Function) => { From 507391facab0869886d819743cce361b5d3b1cfc Mon Sep 17 00:00:00 2001 From: AhmedHamed3699 Date: Thu, 19 Dec 2024 23:34:57 +0200 Subject: [PATCH 30/54] hotfix(chats): fix create group again --- src/sockets/chats.ts | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/sockets/chats.ts b/src/sockets/chats.ts index c8f3f11..597736f 100644 --- a/src/sockets/chats.ts +++ b/src/sockets/chats.ts @@ -176,15 +176,14 @@ const handleCreatePrivateChat = async ( await Promise.all([ joinRoom(io, newChat._id as string, memberId), joinRoom(io, newChat._id as string, senderId), + informSessions(io, memberId, newChat, 'JOIN_PRIVATE_CHAT'), + informSessions(io, senderId, newChat, 'JOIN_PRIVATE_CHAT'), User.updateMany( { _id: { $in: [memberId, senderId] } }, { $push: { chats: { chat: newChat._id } } }, { new: true } ), ]); - socket - .to(newChat._id as string) - .emit('JOIN_PRIVATE_CHAT', { chatId: newChat._id as string }); ack({ success: true, @@ -237,18 +236,16 @@ const handleCreateGroupChannel = async ( }); await newChat.save(); await Promise.all([ - allMembers.map(async (member) => - joinRoom(io, newChat._id as string, member.user) - ), + allMembers.map(async (member) => { + joinRoom(io, newChat._id as string, member.user); + informSessions(io, member.user, newChat, 'JOIN_GROUP_CHANNEL'); + }), User.updateMany( { _id: { $in: allMembers.map((member) => member.user) } }, { $push: { chats: { chat: newChat._id } } }, { new: true } ), ]); - socket - .to(newChat._id as string) - .emit('JOIN_GROUP_CHANNEL', { chatId: newChat._id as string }); ack({ success: true, From 83d914998849fb2bfce9c1d1fb856a930f4657bf Mon Sep 17 00:00:00 2001 From: Peter Safwat <118059454+PeterSafwatHBK@users.noreply.github.com> Date: Thu, 19 Dec 2024 23:37:29 +0200 Subject: [PATCH 31/54] feat(search): groups-channels-picture --- src/controllers/searchController.ts | 31 ++++++++++++++++++++++++----- src/types/message.ts | 2 ++ 2 files changed, 28 insertions(+), 5 deletions(-) diff --git a/src/controllers/searchController.ts b/src/controllers/searchController.ts index 42879e4..e5b34ac 100644 --- a/src/controllers/searchController.ts +++ b/src/controllers/searchController.ts @@ -7,6 +7,7 @@ import User from '@base/models/userModel'; import IUser from '@base/types/user'; import IChat from '@base/types/chat'; import IGroupChannel from '@base/types/groupChannel'; +import IMessage from '@base/types/message'; export const searchMessages = catchAsync(async (req: any, res: Response, next: NextFunction) => { try { let globalSearchResult: { @@ -67,17 +68,37 @@ export const searchMessages = catchAsync(async (req: any, res: Response, next: N // Fetch messages and populate references const messages = await Message.find(finalSearchConditions) + .populate('senderId', 'username') .populate({ - path: 'senderId', - select: 'username screenFirstName screenLastName phoneNumber photo bio accountStatus stories', + path: 'chatId', + select: 'name type', }) + .limit(50); + + let groups: string[] = []; // Explicitly typing as string array + messages.forEach((message: IMessage) => { + groups.push(message.chatId.name); + console.log(message.chatId.name); + }); + + // Search for group channels by name in the groups array + const groupChannels = await GroupChannel.find({ + name: { $in: groups }, + }) + .select('name type picture'); + + // Now, populate the chatId with name, type, and picture + const updatedMessages = await Message.find(finalSearchConditions) .populate({ path: 'chatId', - select: 'name type', + select: 'name type picture', + match: { name: { $in: groups } }, // Ensure the chatId matches the groups array }) .limit(50); - + + + // This will print the 'name' from the chatId // Global Search for Groups, Channels, and Chats if (isGlobalSearch) { // Groups and Channels by name @@ -113,7 +134,7 @@ export const searchMessages = catchAsync(async (req: any, res: Response, next: N res.status(200).json({ success: true, data: { - searchResult: messages, + searchResult: updatedMessages, globalSearchResult: globalSearchResult, }, }); diff --git a/src/types/message.ts b/src/types/message.ts index 359aa62..ccfad88 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -11,6 +11,8 @@ interface IMessage extends ICommunication { isAnnouncement: boolean; deliveredTo: Types.ObjectId[]; readBy: Types.ObjectId[]; + senderId: Types.ObjectId; + chatId: Types.groupChannel; parentMessageId: Types.ObjectId | undefined; threadMessages: Types.ObjectId[]; } From 1c6e90072609638b6a2112df70a22fe2933242da Mon Sep 17 00:00:00 2001 From: AhmedHamed3699 Date: Fri, 20 Dec 2024 15:42:17 +0200 Subject: [PATCH 32/54] hotfix(sockets): solved small bug --- package-lock.json | 1756 ++++++++++++++++++++------------------- src/sockets/chats.ts | 4 +- src/sockets/messages.ts | 2 +- 3 files changed, 914 insertions(+), 848 deletions(-) diff --git a/package-lock.json b/package-lock.json index a6d45e7..32dea14 100644 --- a/package-lock.json +++ b/package-lock.json @@ -185,9 +185,9 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.2.tgz", - "integrity": "sha512-Z0WgzSEa+aUcdiJuCIqgujCshpMWgUpgOxXotrYPSA53hA3qopNaqcJpyr0hVb1FeWdnqFA35/fUtXgBK8srQg==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.3.tgz", + "integrity": "sha512-nHIxvKPniQXpmQLb0vhY3VaFb3S0YrTAwpOWJZh1wn3oJPjJk9Asva204PsBdmAE8vpzfHudT8DB0scYvy9q0g==", "dev": true, "engines": { "node": ">=6.9.0" @@ -224,13 +224,13 @@ } }, "node_modules/@babel/generator": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.2.tgz", - "integrity": "sha512-zevQbhbau95nkoxSq3f/DC/SC+EEOUZd3DYqfSkMhY2/wfSeaHV1Ew4vk8e+x8lja31IbyuUa2uQ3JONqKbysw==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", + "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", "dev": true, "dependencies": { - "@babel/parser": "^7.26.2", - "@babel/types": "^7.26.0", + "@babel/parser": "^7.26.3", + "@babel/types": "^7.26.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -251,19 +251,6 @@ "node": ">=6.9.0" } }, - "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.25.9.tgz", - "integrity": "sha512-C47lC7LIDCnz0h4vai/tpNOI95tCd5ZT3iBt/DBH5lXKHZsyNQv18yf1wIIg2ntiQNgmAvA+DgZ82iW8Qdym8g==", - "dev": true, - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-compilation-targets": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.9.tgz", @@ -302,13 +289,13 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.25.9.tgz", - "integrity": "sha512-ORPNZ3h6ZRkOyAa/SaHU+XsLZr0UQzRwuDQ0cczIA17nAzZ+85G5cVkOJIj7QavLZGSe8QXUmNFxSZzjcZF9bw==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz", + "integrity": "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", - "regexpu-core": "^6.1.1", + "regexpu-core": "^6.2.0", "semver": "^6.3.1" }, "engines": { @@ -319,9 +306,9 @@ } }, "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", - "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", + "integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==", "dev": true, "dependencies": { "@babel/helper-compilation-targets": "^7.22.6", @@ -432,19 +419,6 @@ "@babel/core": "^7.0.0" } }, - "node_modules/@babel/helper-simple-access": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.25.9.tgz", - "integrity": "sha512-c6WHXuiaRsJTyHYLJV75t9IqsmTbItYfdj99PnzYGQZkYKvan5/2jKJ7gu31J3/BJ/A18grImSPModuyG/Eo0Q==", - "dev": true, - "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - } - }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { "version": "7.25.9", "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", @@ -513,12 +487,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.2.tgz", - "integrity": "sha512-DWMCZH9WA4Maitz2q21SRKHo9QXZxkDsbNZoVD62gusNtNBBqDg9i7uOhASfTfIGNzW+O+r7+jAlM8dwphcJKQ==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.3.tgz", + "integrity": "sha512-WJ/CvmY8Mea8iDXo6a7RK2wbmJITT5fN3BEkRuFlxVyNx8jOKIIhmC4fSkTcPcf8JyavbBwIe6OpiCOBXt/IcA==", "dev": true, "dependencies": { - "@babel/types": "^7.26.0" + "@babel/types": "^7.26.3" }, "bin": { "parser": "bin/babel-parser.js" @@ -1105,12 +1079,11 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.25.9.tgz", - "integrity": "sha512-KRhdhlVk2nObA5AYa7QMgTMTVJdfHprfpAk4DjZVtllqRg9qarilstTKEhpVjyt+Npi8ThRyiV8176Am3CodPA==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", + "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", "dev": true, "dependencies": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.25.9", "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { @@ -1245,14 +1218,13 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.25.9.tgz", - "integrity": "sha512-dwh2Ol1jWwL2MgkCzUSOvfmKElqQcuswAZypBSUsScMXvgdT8Ekq5YA6TtqpTVWH+4903NmboMuH1o9i8Rxlyg==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", + "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", "dev": true, "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-simple-access": "^7.25.9" + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helper-plugin-utils": "^7.25.9" }, "engines": { "node": ">=6.9.0" @@ -1607,9 +1579,9 @@ } }, "node_modules/@babel/plugin-transform-typescript": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.25.9.tgz", - "integrity": "sha512-7PbZQZP50tzv2KGGnhh82GSyMB01yKY9scIjf1a+GfZCtInOWqUH5+1EBU4t9fyR5Oykkkc9vFTs4OHrhHXljQ==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.26.3.tgz", + "integrity": "sha512-6+5hpdr6mETwSKjmJUdYw0EIkATiQhnELWlE3kJFBwSg/BGIVwVaVbX+gOXBCdc7Ln1RXZxyWGecIXhUfnl7oA==", "dev": true, "dependencies": { "@babel/helper-annotate-as-pure": "^7.25.9", @@ -1831,16 +1803,16 @@ } }, "node_modules/@babel/traverse": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.9.tgz", - "integrity": "sha512-ZCuvfwOwlz/bawvAuvcj8rrithP2/N55Tzz342AkTvq4qaWbGfmCk/tKhNaV2cthijKrPAA8SRJV5WWe7IBMJw==", + "version": "7.26.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.4.tgz", + "integrity": "sha512-fH+b7Y4p3yqvApJALCPJcwb0/XaOSgtK4pzV6WVjPR5GLFQBRI7pfoX2V2iM48NXvX07NUxxm1Vw98YjqTcU5w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/generator": "^7.25.9", - "@babel/parser": "^7.25.9", + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.3", + "@babel/parser": "^7.26.3", "@babel/template": "^7.25.9", - "@babel/types": "^7.25.9", + "@babel/types": "^7.26.3", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -1858,9 +1830,9 @@ } }, "node_modules/@babel/types": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.0.tgz", - "integrity": "sha512-Z/yiTPj+lDVnF7lWeKCIJzaIkI0vYO87dMpZ4bg4TDrFe4XXLFWL1TbXU27gBP3QccxV9mZICCrnjnYlJjXHOA==", + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.3.tgz", + "integrity": "sha512-vN5p+1kl59GVKMvTHt55NzzmYVxprfJD+ql7U9NFIfKCBkYE55LYtS+WtPlaYOyzydrKI8Nezd+aZextrd+FMA==", "dev": true, "dependencies": { "@babel/helper-string-parser": "^7.25.9", @@ -2419,9 +2391,9 @@ } }, "node_modules/@faker-js/faker": { - "version": "9.2.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.2.0.tgz", - "integrity": "sha512-ulqQu4KMr1/sTFIYvqSdegHT8NIkt66tFAkugGnHA+1WAfEn6hMzNR+svjXGFRVLnapxvej67Z/LwchFrnLBUg==", + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.3.0.tgz", + "integrity": "sha512-r0tJ3ZOkMd9xsu3VRfqlFR6cz0V/jFYRswAIpC+m/DIfAUXq7g8N7wTAlhSANySXYGKzGryfDXwtwsY8TxEIDw==", "funding": [ { "type": "opencollective", @@ -2822,9 +2794,9 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "version": "0.3.8", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", + "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "dev": true, "dependencies": { "@jridgewell/set-array": "^1.2.1", @@ -3077,54 +3049,54 @@ "hasInstallScript": true }, "node_modules/@shikijs/core": { - "version": "1.23.1", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.23.1.tgz", - "integrity": "sha512-NuOVgwcHgVC6jBVH5V7iblziw6iQbWWHrj5IlZI3Fqu2yx9awH7OIQkXIcsHsUmY19ckwSgUMgrqExEyP5A0TA==", + "version": "1.24.3", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-1.24.3.tgz", + "integrity": "sha512-VRcf4GYUIkxIchGM9DrapRcxtgojg4IWKUtX5EtW+4PJiGzF2xQqZSv27PJt+WLc18KT3CNLpNWow9JYV5n+Rg==", "dev": true, "dependencies": { - "@shikijs/engine-javascript": "1.23.1", - "@shikijs/engine-oniguruma": "1.23.1", - "@shikijs/types": "1.23.1", - "@shikijs/vscode-textmate": "^9.3.0", + "@shikijs/engine-javascript": "1.24.3", + "@shikijs/engine-oniguruma": "1.24.3", + "@shikijs/types": "1.24.3", + "@shikijs/vscode-textmate": "^9.3.1", "@types/hast": "^3.0.4", - "hast-util-to-html": "^9.0.3" + "hast-util-to-html": "^9.0.4" } }, "node_modules/@shikijs/engine-javascript": { - "version": "1.23.1", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.23.1.tgz", - "integrity": "sha512-i/LdEwT5k3FVu07SiApRFwRcSJs5QM9+tod5vYCPig1Ywi8GR30zcujbxGQFJHwYD7A5BUqagi8o5KS+LEVgBg==", + "version": "1.24.3", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-1.24.3.tgz", + "integrity": "sha512-De8tNLvYjeK6V0Gb47jIH2M+OKkw+lWnSV1j3HVDFMlNIglmVcTMG2fASc29W0zuFbfEEwKjO8Fe4KYSO6Ce3w==", "dev": true, "dependencies": { - "@shikijs/types": "1.23.1", - "@shikijs/vscode-textmate": "^9.3.0", - "oniguruma-to-es": "0.4.1" + "@shikijs/types": "1.24.3", + "@shikijs/vscode-textmate": "^9.3.1", + "oniguruma-to-es": "0.8.0" } }, "node_modules/@shikijs/engine-oniguruma": { - "version": "1.23.1", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.23.1.tgz", - "integrity": "sha512-KQ+lgeJJ5m2ISbUZudLR1qHeH3MnSs2mjFg7bnencgs5jDVPeJ2NVDJ3N5ZHbcTsOIh0qIueyAJnwg7lg7kwXQ==", + "version": "1.24.3", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-1.24.3.tgz", + "integrity": "sha512-iNnx950gs/5Nk+zrp1LuF+S+L7SKEhn8k9eXgFYPGhVshKppsYwRmW8tpmAMvILIMSDfrgqZ0w+3xWVQB//1Xw==", "dev": true, "dependencies": { - "@shikijs/types": "1.23.1", - "@shikijs/vscode-textmate": "^9.3.0" + "@shikijs/types": "1.24.3", + "@shikijs/vscode-textmate": "^9.3.1" } }, "node_modules/@shikijs/types": { - "version": "1.23.1", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.23.1.tgz", - "integrity": "sha512-98A5hGyEhzzAgQh2dAeHKrWW4HfCMeoFER2z16p5eJ+vmPeF6lZ/elEne6/UCU551F/WqkopqRsr1l2Yu6+A0g==", + "version": "1.24.3", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-1.24.3.tgz", + "integrity": "sha512-FPMrJ69MNxhRtldRk69CghvaGlbbN3pKRuvko0zvbfa2dXp4pAngByToqS5OY5jvN8D7LKR4RJE8UvzlCOuViw==", "dev": true, "dependencies": { - "@shikijs/vscode-textmate": "^9.3.0", + "@shikijs/vscode-textmate": "^9.3.1", "@types/hast": "^3.0.4" } }, "node_modules/@shikijs/vscode-textmate": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.3.0.tgz", - "integrity": "sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==", + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.3.1.tgz", + "integrity": "sha512-79QfK1393x9Ho60QFyLti+QfdJzRQCVLFb97kOIV7Eo9vQU/roINgk7m24uv0a7AUvN//RDH36FLjjK48v0s9g==", "dev": true }, "node_modules/@sinclair/typebox": { @@ -3285,9 +3257,9 @@ } }, "node_modules/@types/express-serve-static-core": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.1.tgz", - "integrity": "sha512-CRICJIl0N5cXDONAdlTv5ShATZ4HEwk6kDDIW2/w9qOWKg+NU/5F8wYRWCrONad0/UKkloNSmmyN/wX4rtpbVA==", + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.2.tgz", + "integrity": "sha512-vluaspfvWEtE4vcSDlKRNer52DvOGrB2xv6diXy6UKyKW0lqZiWHGNApSyxOv+8DE5Z27IzVvE7hNkxg7EXIcg==", "dev": true, "dependencies": { "@types/node": "*", @@ -3297,9 +3269,9 @@ } }, "node_modules/@types/express-session": { - "version": "1.18.0", - "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.0.tgz", - "integrity": "sha512-27JdDRgor6PoYlURY+Y5kCakqp5ulC0kmf7y+QwaY+hv9jEFuQOThgkjyA53RP3jmKuBsH5GR6qEfFmvb8mwOA==", + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/@types/express-session/-/express-session-1.18.1.tgz", + "integrity": "sha512-S6TkD/lljxDlQ2u/4A70luD8/ZxZcrU5pQwI1rVXCiaVIywoFgbA+PIUNDjPhQpPdK0dGleLtYc/y7XWBfclBg==", "dev": true, "dependencies": { "@types/express": "*" @@ -3426,17 +3398,17 @@ } }, "node_modules/@types/node": { - "version": "22.9.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.9.0.tgz", - "integrity": "sha512-vuyHg81vvWA1Z1ELfvLko2c8f34gyA0zaic0+Rllc5lbCnbSyuvb2Oxpm6TAUAC/2xZN3QGqxBNggD1nNR2AfQ==", + "version": "22.10.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.10.2.tgz", + "integrity": "sha512-Xxr6BBRCAOQixvonOye19wnzyDiUtTeqldOOmj3CkeblonbccA12PFwlufvRdrpjXxqnmUaeiU5EOA+7s5diUQ==", "dependencies": { - "undici-types": "~6.19.8" + "undici-types": "~6.20.0" } }, "node_modules/@types/nodemailer": { - "version": "6.4.16", - "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.16.tgz", - "integrity": "sha512-uz6hN6Pp0upXMcilM61CoKyjT7sskBoOWpptkjjJp8jIMlTdc3xG01U7proKkXzruMS4hS0zqtHNkNPFB20rKQ==", + "version": "6.4.17", + "resolved": "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.17.tgz", + "integrity": "sha512-I9CCaIp6DTldEg7vyUTZi8+9Vo0hi1/T8gv3C89yk1rSAAzoKQ8H8ki/jBYJSFoH/BisgLP8tkZMlQ91CIquww==", "dev": true, "dependencies": { "@types/node": "*" @@ -3613,16 +3585,16 @@ "dev": true }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.13.0.tgz", - "integrity": "sha512-nQtBLiZYMUPkclSeC3id+x4uVd1SGtHuElTxL++SfP47jR0zfkZBJHc+gL4qPsgTuypz0k8Y2GheaDYn6Gy3rg==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.18.1.tgz", + "integrity": "sha512-Ncvsq5CT3Gvh+uJG0Lwlho6suwDfUXH0HztslDf5I+F2wAFAZMRwYLEorumpKLzmO2suAXZ/td1tBg4NZIi9CQ==", "dev": true, "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.13.0", - "@typescript-eslint/type-utils": "8.13.0", - "@typescript-eslint/utils": "8.13.0", - "@typescript-eslint/visitor-keys": "8.13.0", + "@typescript-eslint/scope-manager": "8.18.1", + "@typescript-eslint/type-utils": "8.18.1", + "@typescript-eslint/utils": "8.18.1", + "@typescript-eslint/visitor-keys": "8.18.1", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -3637,24 +3609,20 @@ }, "peerDependencies": { "@typescript-eslint/parser": "^8.0.0 || ^8.0.0-alpha.0", - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.13.0.tgz", - "integrity": "sha512-w0xp+xGg8u/nONcGw1UXAr6cjCPU1w0XVyBs6Zqaj5eLmxkKQAByTdV/uGgNN5tVvN/kKpoQlP2cL7R+ajZZIQ==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.18.1.tgz", + "integrity": "sha512-rBnTWHCdbYM2lh7hjyXqxk70wvon3p2FyaniZuey5TrcGBpfhVp0OxOa6gxr9Q9YhZFKyfbEnxc24ZnVbbUkCA==", "dev": true, "dependencies": { - "@typescript-eslint/scope-manager": "8.13.0", - "@typescript-eslint/types": "8.13.0", - "@typescript-eslint/typescript-estree": "8.13.0", - "@typescript-eslint/visitor-keys": "8.13.0", + "@typescript-eslint/scope-manager": "8.18.1", + "@typescript-eslint/types": "8.18.1", + "@typescript-eslint/typescript-estree": "8.18.1", + "@typescript-eslint/visitor-keys": "8.18.1", "debug": "^4.3.4" }, "engines": { @@ -3665,22 +3633,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.13.0.tgz", - "integrity": "sha512-XsGWww0odcUT0gJoBZ1DeulY1+jkaHUciUq4jKNv4cpInbvvrtDoyBH9rE/n2V29wQJPk8iCH1wipra9BhmiMA==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.18.1.tgz", + "integrity": "sha512-HxfHo2b090M5s2+/9Z3gkBhI6xBH8OJCFjH9MhQ+nnoZqxU3wNxkLT+VWXWSFWc3UF3Z+CfPAyqdCTdoXtDPCQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.13.0", - "@typescript-eslint/visitor-keys": "8.13.0" + "@typescript-eslint/types": "8.18.1", + "@typescript-eslint/visitor-keys": "8.18.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3691,13 +3655,13 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.13.0.tgz", - "integrity": "sha512-Rqnn6xXTR316fP4D2pohZenJnp+NwQ1mo7/JM+J1LWZENSLkJI8ID8QNtlvFeb0HnFSK94D6q0cnMX6SbE5/vA==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.18.1.tgz", + "integrity": "sha512-jAhTdK/Qx2NJPNOTxXpMwlOiSymtR2j283TtPqXkKBdH8OAMmhiUfP0kJjc/qSE51Xrq02Gj9NY7MwK+UxVwHQ==", "dev": true, "dependencies": { - "@typescript-eslint/typescript-estree": "8.13.0", - "@typescript-eslint/utils": "8.13.0", + "@typescript-eslint/typescript-estree": "8.18.1", + "@typescript-eslint/utils": "8.18.1", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -3708,16 +3672,15 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.13.0.tgz", - "integrity": "sha512-4cyFErJetFLckcThRUFdReWJjVsPCqyBlJTi6IDEpc1GWCIIZRFxVppjWLIMcQhNGhdWJJRYFHpHoDWvMlDzng==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.18.1.tgz", + "integrity": "sha512-7uoAUsCj66qdNQNpH2G8MyTFlgerum8ubf21s3TSM3XmKXuIn+H2Sifh/ES2nPOPiYSRJWAk0fDkW0APBWcpfw==", "dev": true, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3728,13 +3691,13 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.13.0.tgz", - "integrity": "sha512-v7SCIGmVsRK2Cy/LTLGN22uea6SaUIlpBcO/gnMGT/7zPtxp90bphcGf4fyrCQl3ZtiBKqVTG32hb668oIYy1g==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.18.1.tgz", + "integrity": "sha512-z8U21WI5txzl2XYOW7i9hJhxoKKNG1kcU4RzyNvKrdZDmbjkmLBo8bgeiOJmA06kizLI76/CCBAAGlTlEeUfyg==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.13.0", - "@typescript-eslint/visitor-keys": "8.13.0", + "@typescript-eslint/types": "8.18.1", + "@typescript-eslint/visitor-keys": "8.18.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -3749,10 +3712,8 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { @@ -3768,15 +3729,15 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.13.0.tgz", - "integrity": "sha512-A1EeYOND6Uv250nybnLZapeXpYMl8tkzYUxqmoKAWnI4sei3ihf2XdZVd+vVOmHGcp3t+P7yRrNsyyiXTvShFQ==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.18.1.tgz", + "integrity": "sha512-8vikiIj2ebrC4WRdcAdDcmnu9Q/MXXwg+STf40BVfT8exDqBCUPdypvzcUPxEqRGKg9ALagZ0UWcYCtn+4W2iQ==", "dev": true, "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.13.0", - "@typescript-eslint/types": "8.13.0", - "@typescript-eslint/typescript-estree": "8.13.0" + "@typescript-eslint/scope-manager": "8.18.1", + "@typescript-eslint/types": "8.18.1", + "@typescript-eslint/typescript-estree": "8.18.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3786,17 +3747,18 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0" + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.13.0.tgz", - "integrity": "sha512-7N/+lztJqH4Mrf0lb10R/CbI1EaAMMGyF5y0oJvFoAhafwgiRA7TXyd8TFn8FC8k5y2dTsYogg238qavRGNnlw==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.18.1.tgz", + "integrity": "sha512-Vj0WLm5/ZsD013YeUKn+K0y8p1M0jPpxOkKdbD1wB0ns53a5piVY02zjf072TblEweAbcYiFiPoSMF3kp+VhhQ==", "dev": true, "dependencies": { - "@typescript-eslint/types": "8.13.0", - "eslint-visitor-keys": "^3.4.3" + "@typescript-eslint/types": "8.18.1", + "eslint-visitor-keys": "^4.2.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -3806,10 +3768,22 @@ "url": "https://opencollective.com/typescript-eslint" } }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", + "integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", + "dev": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, "node_modules/@ungap/structured-clone": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.0.tgz", - "integrity": "sha512-zuVdFrMJiuCDQUMCzQaD6KL28MjnqqN8XnAqiEq9PNm/hCPTSGfrXCOfwj1ow4LFb/tNymJPwsNbVePc1xFqrQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.2.1.tgz", + "integrity": "sha512-fEzPV3hSkSMltkw152tJKNARhOupqbH96MZWyRjNaYZOMIzbrTeQDG+MTc6Mr2pgzFQzFxAfmhGDNP5QK++2ZA==", "dev": true }, "node_modules/abbrev": { @@ -3915,7 +3889,6 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -4089,15 +4062,15 @@ } }, "node_modules/array.prototype.flat": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", - "integrity": "sha512-djYB+Zx2vLewY8RWlNCUdHjDXs2XOgm602S9E7P/UpHgfeHL00cRiIF+IN/G/aUJ7kGPb6yO/ErDI5V2s8iycA==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4107,15 +4080,15 @@ } }, "node_modules/array.prototype.flatmap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.2.tgz", - "integrity": "sha512-Ewyx0c9PmpcsByhSW4r+9zDU7sGjFc86qf/kKtuSCRdhfbk0SNLLkaT5qvcHnRGgc5NP/ly/y+qkXkqONX54CQ==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "es-shim-unscopables": "^1.0.0" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -4141,19 +4114,18 @@ } }, "node_modules/arraybuffer.prototype.slice": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.3.tgz", - "integrity": "sha512-bMxMKAjg13EBSVscxTaYA4mRc5t1UAXa2kXiGTNfZ079HIWXEkKmkgFrh/nJqamaLSrXO5H4WFFkPEaLJWbs3A==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.1", - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", - "es-abstract": "^1.22.3", - "es-errors": "^1.2.1", - "get-intrinsic": "^1.2.3", - "is-array-buffer": "^3.0.4", - "is-shared-array-buffer": "^1.0.2" + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" }, "engines": { "node": ">= 0.4" @@ -4238,9 +4210,9 @@ } }, "node_modules/axios": { - "version": "1.7.7", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.7.tgz", - "integrity": "sha512-S4kL7XrjgBmvdGut0sN3yJxqYzrDOnivkBiN0OFs6hLiUam3UPvswUo0kqGyhqUZGEOytHyumEdXsAkgCOUf3Q==", + "version": "1.7.9", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", + "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -4309,13 +4281,13 @@ } }, "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.11", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", - "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz", + "integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==", "dev": true, "dependencies": { "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.6.2", + "@babel/helper-define-polyfill-provider": "^0.6.3", "semver": "^6.3.1" }, "peerDependencies": { @@ -4336,12 +4308,12 @@ } }, "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", - "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz", + "integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==", "dev": true, "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.2" + "@babel/helper-define-polyfill-provider": "^0.6.3" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -4499,7 +4471,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", - "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -4517,9 +4488,9 @@ } }, "node_modules/browserslist": { - "version": "4.24.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.2.tgz", - "integrity": "sha512-ZIc+Q62revdMcqC6aChtW4jz3My3klmCO1fEmINZY/8J3EpBg5/A/D0AKmBveUh6pgoeycoMkVMko84tuYS+Gg==", + "version": "4.24.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.3.tgz", + "integrity": "sha512-1CPmv8iobE2fyRMV97dAcMVegvvWKxmq94hkLiAkUGwKVTyDLw33K+ZxiFrREKmmps4rIw6grcCFCnTMSZ/YiA==", "dev": true, "funding": [ { @@ -4536,9 +4507,9 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001669", - "electron-to-chromium": "^1.5.41", - "node-releases": "^2.0.18", + "caniuse-lite": "^1.0.30001688", + "electron-to-chromium": "^1.5.73", + "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.1" }, "bin": { @@ -4570,10 +4541,9 @@ } }, "node_modules/bson": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.0.tgz", - "integrity": "sha512-ROchNosXMJD2cbQGm84KoP7vOGPO6/bOAW0veMMbzhXLqoZptcaYRVLitwvuhwhjjpU1qP4YZRWLhgETdgqUQw==", - "license": "Apache-2.0", + "version": "6.10.1", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.10.1.tgz", + "integrity": "sha512-P92xmHDQjSKPLHqFxefqMxASNq/aWJMEZugpCjf+AF/pgcUpMMQCg7t7+ewko0/u8AapvF3luf/FoehddEK+sA==", "engines": { "node": ">=16.20.1" } @@ -4608,15 +4578,42 @@ } }, "node_modules/call-bind": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", - "integrity": "sha512-GHTSNSYICQ7scH7sZ+M2rFopRoLh8t2bLSW6BbgrtLsahOIB5iyAVJf9GjWK3cYTDaMj4XdBpM1cA6pIS0Kv2w==", + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dev": true, "dependencies": { + "call-bind-apply-helpers": "^1.0.0", "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.1" + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.1.tgz", + "integrity": "sha512-BhYE+WDaywFg2TBWYNXAE+8B1ATnThNBqXHP5nQu0jWJdVvY2hvkpyB3qOmtmDePiS5/BDQ8wASEWGMWRG148g==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -4649,9 +4646,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001679", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001679.tgz", - "integrity": "sha512-j2YqID/YwpLnKzCmBOS4tlZdWprXm3ZmQLBH9ZBXFOhoxLA46fwyBvx6toCBWBmnuwUY/qB3kEU6gFx8qgCroA==", + "version": "1.0.30001690", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001690.tgz", + "integrity": "sha512-5ExiE3qQN6oF8Clf8ifIDcMRCRE/dMGcETG/XGMD8/XiXm6HXQgQTh1yZYLXXpSOsEUlJm1Xr7kGULZTuGtP/w==", "dev": true, "funding": [ { @@ -4687,7 +4684,6 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -4776,7 +4772,6 @@ "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "dev": true, "funding": [ { "type": "github", @@ -4850,7 +4845,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, "dependencies": { "color-name": "~1.1.4" }, @@ -4861,8 +4855,7 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, "node_modules/color-support": { "version": "1.1.3", @@ -5092,9 +5085,9 @@ "dev": true }, "node_modules/cross-spawn": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.5.tgz", - "integrity": "sha512-ZVJrKKYunU38/76t0RMOulHOnUcbU9GbpWKAOZ0mhjr7CX6FVrH+4FrAapSOekrgFQ3f/8gwMEuIft0aKq6Hug==", + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, "dependencies": { "path-key": "^3.1.0", @@ -5171,12 +5164,12 @@ } }, "node_modules/data-view-byte-offset": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.0.tgz", - "integrity": "sha512-t/Ygsytq+R995EJ5PZlD4Cu56sWa8InXySaViRzw9apusqsOO2bQP+SbYzAhR0pFKoB+43lYy8rWban9JSuXnA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" }, @@ -5204,9 +5197,9 @@ } }, "node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", "dependencies": { "ms": "^2.1.3" }, @@ -5252,6 +5245,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", @@ -5401,9 +5395,9 @@ } }, "node_modules/dotenv": { - "version": "16.4.5", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.5.tgz", - "integrity": "sha512-ZmdL2rui+eB2YwhsWzjInR8LldtZHGDoQ1ugH85ppHKwpUHL7j7rN0Ti9NCnGiQbhaZ11FpR+7ao1dNsmduNUg==", + "version": "16.4.7", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", + "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", "engines": { "node": ">=12" }, @@ -5411,6 +5405,19 @@ "url": "https://dotenvx.com" } }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, "node_modules/dynamic-dedupe": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", @@ -5458,9 +5465,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.55", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.55.tgz", - "integrity": "sha512-6maZ2ASDOTBtjt9FhqYPRnbvKU5tjG0IN9SztUOWYw2AzNDNpKJYLJmlK0/En4Hs/aiWnB+JZ+gW19PIGszgKg==", + "version": "1.5.75", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.75.tgz", + "integrity": "sha512-Lf3++DumRE/QmweGjU+ZcKqQ+3bKkU/qjaKYhIJKEOhgIO9Xs6IiAQFkfFoj+RhgDk4LUeNsLo6plExHqSyu6Q==", "dev": true }, "node_modules/emittery": { @@ -5531,6 +5538,22 @@ "node": ">= 0.6" } }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/enhanced-resolve": { "version": "5.17.1", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.17.1.tgz", @@ -5566,57 +5589,59 @@ } }, "node_modules/es-abstract": { - "version": "1.23.3", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", - "integrity": "sha512-e+HfNH61Bj1X9/jLc5v1owaLYuHdeHHSQlkhCBiTK8rBvKaULl/beGMxwrMXjpYrv4pz22BlY570vVePA2ho4A==", + "version": "1.23.6", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.6.tgz", + "integrity": "sha512-Ifco6n3yj2tMZDWNLyloZrytt9lqqlwvS83P3HtaETR0NUOYnIULGGHpktqYGObGy+8wc1okO25p8TjemhImvA==", "dev": true, "dependencies": { "array-buffer-byte-length": "^1.0.1", - "arraybuffer.prototype.slice": "^1.0.3", + "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "data-view-buffer": "^1.0.1", "data-view-byte-length": "^1.0.1", "data-view-byte-offset": "^1.0.0", - "es-define-property": "^1.0.0", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-set-tostringtag": "^2.0.3", - "es-to-primitive": "^1.2.1", - "function.prototype.name": "^1.1.6", - "get-intrinsic": "^1.2.4", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.7", + "get-intrinsic": "^1.2.6", "get-symbol-description": "^1.0.2", - "globalthis": "^1.0.3", - "gopd": "^1.0.1", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", - "has-proto": "^1.0.3", - "has-symbols": "^1.0.3", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", "hasown": "^2.0.2", - "internal-slot": "^1.0.7", + "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.4", "is-callable": "^1.2.7", - "is-data-view": "^1.0.1", + "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", - "is-regex": "^1.1.4", + "is-regex": "^1.2.1", "is-shared-array-buffer": "^1.0.3", - "is-string": "^1.0.7", + "is-string": "^1.1.1", "is-typed-array": "^1.1.13", - "is-weakref": "^1.0.2", - "object-inspect": "^1.13.1", + "is-weakref": "^1.1.0", + "math-intrinsics": "^1.0.0", + "object-inspect": "^1.13.3", "object-keys": "^1.1.1", "object.assign": "^4.1.5", - "regexp.prototype.flags": "^1.5.2", - "safe-array-concat": "^1.1.2", - "safe-regex-test": "^1.0.3", - "string.prototype.trim": "^1.2.9", - "string.prototype.trimend": "^1.0.8", + "regexp.prototype.flags": "^1.5.3", + "safe-array-concat": "^1.1.3", + "safe-regex-test": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.2", "typed-array-byte-length": "^1.0.1", - "typed-array-byte-offset": "^1.0.2", - "typed-array-length": "^1.0.6", + "typed-array-byte-offset": "^1.0.3", + "typed-array-length": "^1.0.7", "unbox-primitive": "^1.0.2", - "which-typed-array": "^1.1.15" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -5626,12 +5651,9 @@ } }, "node_modules/es-define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.0.tgz", - "integrity": "sha512-jxayLKShrEqqzJ0eumQbVhTYQM27CfT1T35+gCgDFoL82JLsXqTJ76zv6A0YLOgEnLUMvLzsDsGIrl8NFpT2gQ==", - "dependencies": { - "get-intrinsic": "^1.2.4" - }, + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", "engines": { "node": ">= 0.4" } @@ -5674,7 +5696,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.0.0.tgz", "integrity": "sha512-MZ4iQ6JwHOBQjahnjwaC1ZtIBH+2ohjamzAO3oaHcXYup7qxjF2fixyH+Q71voWHeOkI2q/TnJao/KfXYIZWbw==", - "dev": true, "dependencies": { "es-errors": "^1.3.0" }, @@ -5706,14 +5727,14 @@ } }, "node_modules/es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", "dev": true, "dependencies": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" }, "engines": { "node": ">= 0.4" @@ -5916,19 +5937,19 @@ } }, "node_modules/eslint-import-resolver-typescript": { - "version": "3.6.3", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.6.3.tgz", - "integrity": "sha512-ud9aw4szY9cCT1EWWdGv1L1XR6hh2PaRWif0j2QjQ0pgTY/69iw+W0Z4qZv5wHahOl8isEr+k/JnyAqNQkLkIA==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-3.7.0.tgz", + "integrity": "sha512-Vrwyi8HHxY97K5ebydMtffsWAn1SCR9eol49eCd5fJS4O1WV7PaAjbcjmbfJJSMz/t4Mal212Uz/fQZrOB8mow==", "dev": true, "dependencies": { "@nolyfill/is-core-module": "1.0.39", - "debug": "^4.3.5", + "debug": "^4.3.7", "enhanced-resolve": "^5.15.0", - "eslint-module-utils": "^2.8.1", "fast-glob": "^3.3.2", "get-tsconfig": "^4.7.5", "is-bun-module": "^1.0.2", - "is-glob": "^4.0.3" + "is-glob": "^4.0.3", + "stable-hash": "^0.0.4" }, "engines": { "node": "^14.18.0 || >=16.0.0" @@ -6105,9 +6126,9 @@ } }, "node_modules/eslint-plugin-jest": { - "version": "28.9.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.9.0.tgz", - "integrity": "sha512-rLu1s1Wf96TgUUxSw6loVIkNtUjq1Re7A9QdCCHSohnvXEBAjuL420h0T/fMmkQlNsQP2GhQzEUpYHPfxBkvYQ==", + "version": "28.10.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.10.0.tgz", + "integrity": "sha512-hyMWUxkBH99HpXT3p8hc7REbEZK3D+nk8vHXGgpB+XXsi0gO4PxMSP+pjfUzb67GnV9yawV9a53eUmcde1CCZA==", "dev": true, "dependencies": { "@typescript-eslint/utils": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -6640,9 +6661,9 @@ } }, "node_modules/express": { - "version": "4.21.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.1.tgz", - "integrity": "sha512-YSFlK1Ee0/GC8QaO91tHcDxJiE/X4FbpAyQWkxAvG6AXCuR65YzK8ua6D9hvi/TzUfZMpc+BwuM1IPw8fmQBiQ==", + "version": "4.21.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", + "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", @@ -6663,7 +6684,7 @@ "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", - "path-to-regexp": "0.1.10", + "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", @@ -6678,6 +6699,10 @@ }, "engines": { "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/express-mongo-sanitize": { @@ -6689,9 +6714,9 @@ } }, "node_modules/express-rate-limit": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.4.1.tgz", - "integrity": "sha512-KS3efpnpIDVIXopMc65EMbWbUht7qvTCdtCR2dD/IZmi9MIkopYESwyRqLgv8Pfu589+KqDqOdzJWW7AHoACeg==", + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-7.5.0.tgz", + "integrity": "sha512-eB5zbQh5h+VenMPM3fh+nw1YExi5nMr6HUCR62ELSP11huvxm/Uir1H1QEyTkk5QX6A58pX6NmaTMceKZ0Eodg==", "engines": { "node": ">= 16" }, @@ -6699,7 +6724,7 @@ "url": "https://github.com/sponsors/express-rate-limit" }, "peerDependencies": { - "express": "4 || 5 || ^5.0.0-beta.1" + "express": "^4.11 || 5 || ^5.0.0-beta.1" } }, "node_modules/express-session": { @@ -6970,9 +6995,9 @@ } }, "node_modules/flatted": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.1.tgz", - "integrity": "sha512-X8cqMLLie7KsNUDSdzeN8FYK9rEt4Dt67OsG/DNGnYTSDBG4uFAJFBnUeiV+zCVAvwFy56IjM9sH51jVaEhNxw==", + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.2.tgz", + "integrity": "sha512-AiwGJM8YcNOaobumgtng+6NHuOqC3A7MixFeDafM3X9cIUM+xUXoS5Vfgf+OihAYe20fxqNM9yPBXJzRtZ/4eA==", "dev": true }, "node_modules/fn-args": { @@ -7142,15 +7167,16 @@ } }, "node_modules/function.prototype.name": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.6.tgz", - "integrity": "sha512-Z5kx79swU5P27WEayXM1tBi5Ze/lbIyiNgU3qyXUOf9b2rgXYyF9Dy9Cx+IQv/Lc8WCG6L82zwUPpSS9hGehIg==", + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.7.tgz", + "integrity": "sha512-2g4x+HqTJKM9zcJqBSpjoRmdcPFtJM60J3xJisTQSXBWka5XqyBN/2tNUgma1mztTXyDuUsEtYe5qcs7xYzYQA==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "define-properties": "^1.2.0", - "es-abstract": "^1.22.1", - "functions-have-names": "^1.2.3" + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" }, "engines": { "node": ">= 0.4" @@ -7215,15 +7241,20 @@ } }, "node_modules/get-intrinsic": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", - "integrity": "sha512-5uYhsJH8VJBTv7oslg4BznJYhDoRI6waYCxMmCdnTrcCrHA/fCFKoTFz2JKKE0HdDFUF7/oQuhzumXJK7paBRQ==", + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.6.tgz", + "integrity": "sha512-qxsEs+9A+u85HhllWJJFicJfPDhRmjzoYdl64aMWW9yRIJmSyxdn8IEkuIM530/7T+lv0TIHd8L6Q/ra0tEoeA==", "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "dunder-proto": "^1.0.0", + "es-define-property": "^1.0.1", "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", "function-bind": "^1.1.2", - "has-proto": "^1.0.1", - "has-symbols": "^1.0.3", - "hasown": "^2.0.0" + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.0.0" }, "engines": { "node": ">= 0.4" @@ -7254,14 +7285,14 @@ } }, "node_modules/get-symbol-description": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.0.2.tgz", - "integrity": "sha512-g0QYk1dZBxGwk+Ngc+ltRH2IBp2f7zBkBMBJZCDerh6EhlhSR6+9irMCuT/09zD6qkarHUSn529sK/yL4S27mg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", "dev": true, "dependencies": { - "call-bind": "^1.0.5", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4" + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -7343,9 +7374,9 @@ } }, "node_modules/globals": { - "version": "15.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-15.12.0.tgz", - "integrity": "sha512-1+gLErljJFhbOVyaetcwJiJ4+eLe45S2E7P5UiZ9xGfeq3ATQf5DOv9G7MH3gGbKQLkzmNh2DxfZwLdw+j6oTQ==", + "version": "15.14.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-15.14.0.tgz", + "integrity": "sha512-OkToC372DtlQeje9/zHIo5CT8lRP/FUgEOKBEhU4e0abL7J7CD24fD9ohiLN5hagG/kWCYj4K5oaxxtj2Z0Dig==", "dev": true, "engines": { "node": ">=18" @@ -7391,11 +7422,11 @@ } }, "node_modules/gopd": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", - "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", - "dependencies": { - "get-intrinsic": "^1.1.3" + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7404,8 +7435,7 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "dev": true + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" }, "node_modules/graphemer": { "version": "1.4.0", @@ -7435,10 +7465,13 @@ } }, "node_modules/has-bigints": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz", - "integrity": "sha512-tSvCKtBr9lkF0Ex0aQiP9N+OpV4zi2r/Nee5VkRDbaqv35RLYMzbwQfFSZZH0kR+Rd6302UJZ2p/bJCEoR3VoQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7447,7 +7480,6 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true, "engines": { "node": ">=8" } @@ -7456,6 +7488,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, "dependencies": { "es-define-property": "^1.0.0" }, @@ -7464,9 +7497,13 @@ } }, "node_modules/has-proto": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.3.tgz", - "integrity": "sha512-SJ1amZAJUiZS+PhsVLf5tGydlaVB8EdFpaSO4gmiUKUOxk8qzn5AIy4ZeJUmh22znIdk/uMAUT2pl3FxzVUH+Q==", + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.0" + }, "engines": { "node": ">= 0.4" }, @@ -7475,9 +7512,9 @@ } }, "node_modules/has-symbols": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", - "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", "engines": { "node": ">= 0.4" }, @@ -7517,9 +7554,9 @@ } }, "node_modules/hast-util-to-html": { - "version": "9.0.3", - "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.3.tgz", - "integrity": "sha512-M17uBDzMJ9RPCqLMO92gNNUDuBSq10a25SDBI08iCCxmorf4Yy6sYHK57n9WAbRAAaU+DuR4W6GN9K4DFZesYg==", + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.4.tgz", + "integrity": "sha512-wxQzXtdbhiwGAUKrnQJXlOPmHnEehzphwkK7aluUPQ+lEc1xefC8pblMgpp2w5ldBTEfveRIrADcrhGIWrlTDA==", "dev": true, "dependencies": { "@types/hast": "^3.0.0", @@ -7720,7 +7757,6 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "dev": true, "engines": { "node": ">=0.8.19" } @@ -7741,14 +7777,14 @@ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" }, "node_modules/internal-slot": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.7.tgz", - "integrity": "sha512-NGnrKwXzSms2qUUih/ILZ5JBqNTSa1+ZmP6flaIp6KmSElgE9qdndzS3cqjrDovwFdmwsGsLdeFgB6suw+1e9g==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", "dev": true, "dependencies": { "es-errors": "^1.3.0", - "hasown": "^2.0.0", - "side-channel": "^1.0.4" + "hasown": "^2.0.2", + "side-channel": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -7763,13 +7799,14 @@ } }, "node_modules/is-array-buffer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.4.tgz", - "integrity": "sha512-wcjaerHw0ydZwfhiKbXJWLDY8A7yV7KhjQOpb83hGgGfId/aQa4TOvwyzn2PuswW2gPCYEL/nEAiSVpdOj1lXw==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.2.1" + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -7800,12 +7837,15 @@ } }, "node_modules/is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", "dev": true, "dependencies": { - "has-bigints": "^1.0.1" + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -7824,13 +7864,13 @@ } }, "node_modules/is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.1.tgz", + "integrity": "sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -7840,9 +7880,9 @@ } }, "node_modules/is-bun-module": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.2.1.tgz", - "integrity": "sha512-AmidtEM6D6NmUiLOvvU7+IePxjEjOzra2h0pSrsfSAcXwl/83zLLXDByafUJy9k/rKK0pvXMLdwKwGHlX2Ke6Q==", + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-1.3.0.tgz", + "integrity": "sha512-DgXeu5UWI0IsMQundYb5UAOzm6G2eVnarJ0byP6Tm55iZNKceD59LNPA2L4VvsScTtHcw0yEkVwSf7PC+QoLSA==", "dev": true, "dependencies": { "semver": "^7.6.3" @@ -7873,9 +7913,9 @@ } }, "node_modules/is-core-module": { - "version": "2.15.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.15.1.tgz", - "integrity": "sha512-z0vtXSwucUJtANQWldhbtbt7BnL0vxiFjIdDLAatwhDYty2bad6s+rijD6Ri4YuYJubLzIJLUidCh09e1djEVQ==", + "version": "2.16.0", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.0.tgz", + "integrity": "sha512-urTSINYfAYgcbLb0yDQ6egFm6h3Mo1DcF9EkyXSRjjzdHbsulg01qhwWuXdOoUBuTkbQ80KDboXa0vFJ+BDH+g==", "dev": true, "dependencies": { "hasown": "^2.0.2" @@ -7888,11 +7928,13 @@ } }, "node_modules/is-data-view": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.1.tgz", - "integrity": "sha512-AHkaJrsUVW6wq6JS8y3JnM/GJF/9cf+k20+iDzlSaJrinEo5+7vRiteOSwBhHRiAyQATN1AmY4hwzxJKPmYf+w==", + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", "dev": true, "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" }, "engines": { @@ -7903,12 +7945,13 @@ } }, "node_modules/is-date-object": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.5.tgz", - "integrity": "sha512-9YQaSxsAiSwcvS33MBk3wTCVnWK+HhF8VZR2jRxehM16QcVOdHqPn4VPHmRK4lSr38n9JriurInLcP90xsYNfQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", "dev": true, "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -7927,12 +7970,15 @@ } }, "node_modules/is-finalizationregistry": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.0.2.tgz", - "integrity": "sha512-0by5vtUJs8iFQb5TYUHHPudOR+qXYIMKtiUzvLIZITZUjknFmziyBJuLhVRc+Ds0dREFlskDNJKYIdIzu/9pfw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", "dev": true, "dependencies": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -8016,12 +8062,13 @@ } }, "node_modules/is-number-object": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.7.tgz", - "integrity": "sha512-k1U0IRzLMo7ZlYIfzRu23Oh6MiIFasgpb9X76eqfFZAqwH44UI4KTBvBYIZ1dSL9ZzChTB9ShHfLkR4pdW5krQ==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", "dev": true, "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -8040,13 +8087,15 @@ } }, "node_modules/is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -8068,12 +8117,12 @@ } }, "node_modules/is-shared-array-buffer": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.3.tgz", - "integrity": "sha512-nA2hv5XIhLR3uVzDDfCIknerhx8XUKnstuOERPNNIinXG7v9u+ohXF67vxm4TPTEPU6lm61ZkwP3c9PCB97rhg==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", "dev": true, "dependencies": { - "call-bind": "^1.0.7" + "call-bound": "^1.0.3" }, "engines": { "node": ">= 0.4" @@ -8095,12 +8144,13 @@ } }, "node_modules/is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", "dev": true, "dependencies": { - "has-tostringtag": "^1.0.0" + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -8110,12 +8160,14 @@ } }, "node_modules/is-symbol": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.4.tgz", - "integrity": "sha512-C/CPBqKWnvdcxqIARxyOh4v1UUEOCHpgDa0WYgpKDFMszcrPcffg5uhwSgPCLD2WWxmq6isisz87tzT01tuGhg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", "dev": true, "dependencies": { - "has-symbols": "^1.0.2" + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" }, "engines": { "node": ">= 0.4" @@ -8125,12 +8177,12 @@ } }, "node_modules/is-typed-array": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.13.tgz", - "integrity": "sha512-uZ25/bUAlUY5fR4OKT4rZQEBrzQWYV9ZJYGGsUmEJ6thodVJ1HX64ePQ6Z0qPWP+m+Uq6e9UugrE38jeYsDSMw==", + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", "dev": true, "dependencies": { - "which-typed-array": "^1.1.14" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -8157,25 +8209,28 @@ } }, "node_modules/is-weakref": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.0.2.tgz", - "integrity": "sha512-qctsuLZmIQ0+vSSMfoVvyFe2+GSEvnmZ2ezTup1SBse9+twCCeial6EEi3Nc2KFcf6+qz2FBPnjXsk8xhKSaPQ==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.0.tgz", + "integrity": "sha512-SXM8Nwyys6nT5WP6pltOwKytLV7FqQ4UiibxVmW+EIosHcmCqkkjViTb5SNssDlkCiEYRP1/pdWUKVvZBmsR2Q==", "dev": true, "dependencies": { - "call-bind": "^1.0.2" + "call-bound": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/is-weakset": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.3.tgz", - "integrity": "sha512-LvIm3/KWzS9oRFHugab7d+M/GcBXuXX5xZkzPmN+NxihdQlZUQ4dWuSV1xR/sq6upL1TJEDrfBgRepHFdBtSNQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4" + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" }, "engines": { "node": ">= 0.4" @@ -8192,8 +8247,7 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "dev": true + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" }, "node_modules/isstream": { "version": "0.1.2", @@ -8294,16 +8348,17 @@ } }, "node_modules/iterator.prototype": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.3.tgz", - "integrity": "sha512-FW5iMbeQ6rBGm/oKgzq2aW4KvAGpxPzYES8N4g4xNXUKpL1mclMvOe+76AcLDTvD+Ze+sOpVhgdAQEKF4L9iGQ==", + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.4.tgz", + "integrity": "sha512-x4WH0BWmrMmg4oHHl+duwubhrvczGlyuGAZu3nvrf0UXOfPu8IhZObFEr7DE/iv01YgVZrsOiRcqw2srkKEDIA==", "dev": true, "dependencies": { - "define-properties": "^1.2.1", - "get-intrinsic": "^1.2.1", - "has-symbols": "^1.0.3", - "reflect.getprototypeof": "^1.0.4", - "set-function-name": "^2.0.1" + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "reflect.getprototypeof": "^1.0.8", + "set-function-name": "^2.0.2" }, "engines": { "node": ">= 0.4" @@ -8941,9 +8996,9 @@ "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" }, "node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "bin": { "jsesc": "bin/jsesc" @@ -8961,8 +9016,7 @@ "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "dev": true + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" }, "node_modules/json-schema": { "version": "0.4.0", @@ -9334,6 +9388,14 @@ "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/mdast-util-to-hast": { "version": "13.2.0", "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", @@ -9574,7 +9636,6 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -9646,13 +9707,12 @@ "integrity": "sha512-23g5BFj4zdQL/b6tor7Ji+QY4pEfNH784BMslY9Qb0UnJWRAt+lQGLYmRaM0KDBwIG23ffEBELhZDP2rhi9f/Q==" }, "node_modules/mongodb": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.11.0.tgz", - "integrity": "sha512-yVbPw0qT268YKhG241vAMLaDQAPbRyTgo++odSgGc9kXnzOujQI60Iyj23B9sQQFPSvmNPvMZ3dsFz0aN55KgA==", - "license": "Apache-2.0", + "version": "6.12.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.12.0.tgz", + "integrity": "sha512-RM7AHlvYfS7jv7+BXund/kR64DryVI+cHbVAy9P61fnb1RcWZqOW1/Wj2YhqMCx+MuYhqTRGv7AwHBzmsCKBfA==", "dependencies": { "@mongodb-js/saslprep": "^1.1.9", - "bson": "^6.10.0", + "bson": "^6.10.1", "mongodb-connection-string-url": "^3.0.0" }, "engines": { @@ -9660,7 +9720,7 @@ }, "peerDependencies": { "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.1.0", + "@mongodb-js/zstd": "^1.1.0 || ^2.0.0", "gcp-metadata": "^5.2.0", "kerberos": "^2.0.1", "mongodb-client-encryption": ">=6.0.0 <7", @@ -9701,13 +9761,13 @@ } }, "node_modules/mongoose": { - "version": "8.8.1", - "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.8.1.tgz", - "integrity": "sha512-l7DgeY1szT98+EKU8GYnga5WnyatAu+kOQ2VlVX1Mxif6A0Umt0YkSiksCiyGxzx8SPhGe9a53ND1GD4yVDrPA==", + "version": "8.9.2", + "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-8.9.2.tgz", + "integrity": "sha512-mLWynmZS1v8HTeMxyLhskQncS1SkrjW1eLNuFDYGQMQ/5QrFrxTLNwWXeCRZeKT2lXyaxW8bnJC9AKPT9jYMkw==", "dependencies": { - "bson": "^6.7.0", + "bson": "^6.10.1", "kareem": "2.6.3", - "mongodb": "~6.10.0", + "mongodb": "~6.12.0", "mpath": "0.9.0", "mquery": "5.0.0", "ms": "2.1.3", @@ -9721,52 +9781,6 @@ "url": "https://opencollective.com/mongoose" } }, - "node_modules/mongoose/node_modules/mongodb": { - "version": "6.10.0", - "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.10.0.tgz", - "integrity": "sha512-gP9vduuYWb9ZkDM546M+MP2qKVk5ZG2wPF63OvSRuUbqCR+11ZCAE1mOfllhlAG0wcoJY5yDL/rV3OmYEwXIzg==", - "license": "Apache-2.0", - "dependencies": { - "@mongodb-js/saslprep": "^1.1.5", - "bson": "^6.7.0", - "mongodb-connection-string-url": "^3.0.0" - }, - "engines": { - "node": ">=16.20.1" - }, - "peerDependencies": { - "@aws-sdk/credential-providers": "^3.188.0", - "@mongodb-js/zstd": "^1.1.0", - "gcp-metadata": "^5.2.0", - "kerberos": "^2.0.1", - "mongodb-client-encryption": ">=6.0.0 <7", - "snappy": "^7.2.2", - "socks": "^2.7.1" - }, - "peerDependenciesMeta": { - "@aws-sdk/credential-providers": { - "optional": true - }, - "@mongodb-js/zstd": { - "optional": true - }, - "gcp-metadata": { - "optional": true - }, - "kerberos": { - "optional": true - }, - "mongodb-client-encryption": { - "optional": true - }, - "snappy": { - "optional": true - }, - "socks": { - "optional": true - } - } - }, "node_modules/morgan": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", @@ -9921,9 +9935,9 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", "dev": true }, "node_modules/nodemailer": { @@ -9935,9 +9949,9 @@ } }, "node_modules/nodemon": { - "version": "3.1.7", - "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.7.tgz", - "integrity": "sha512-hLj7fuMow6f0lbB0cD14Lz2xNjwsyruH251Pk4t/yIitCFJbmY1myuLlHm/q06aST4jg6EgAh74PIBBrRqpVAQ==", + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/nodemon/-/nodemon-3.1.9.tgz", + "integrity": "sha512-hdr1oIb2p6ZSxu3PB2JWWYS7ZQ0qvaZsc3hK8DR8f02kRzc8rjYmxAIvdz+aYC+8F2IjNaB7HMcSDg8nQpJxyg==", "dev": true, "dependencies": { "chokidar": "^3.5.2", @@ -10041,9 +10055,9 @@ } }, "node_modules/npm": { - "version": "10.9.1", - "resolved": "https://registry.npmjs.org/npm/-/npm-10.9.1.tgz", - "integrity": "sha512-yJUw03xLqjiv1D52oHeoS5qmOEC5hkJlhP1cWlSrCgshuxWVyFEEK3M3hLC0NwbTaklLTYrhoIanYsuNP5WUKg==", + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/npm/-/npm-10.9.2.tgz", + "integrity": "sha512-iriPEPIkoMYUy3F6f3wwSZAU93E0Eg6cHwIR6jzzOXWSy+SD/rOODEs74cVONHKSx2obXtuUoyidVEhISrisgQ==", "bundleDependencies": [ "@isaacs/string-locale-compare", "@npmcli/arborist", @@ -10119,8 +10133,8 @@ "@npmcli/arborist": "^8.0.0", "@npmcli/config": "^9.0.0", "@npmcli/fs": "^4.0.0", - "@npmcli/map-workspaces": "^4.0.1", - "@npmcli/package-json": "^6.0.1", + "@npmcli/map-workspaces": "^4.0.2", + "@npmcli/package-json": "^6.1.0", "@npmcli/promise-spawn": "^8.0.2", "@npmcli/redact": "^3.0.0", "@npmcli/run-script": "^9.0.1", @@ -10137,7 +10151,7 @@ "graceful-fs": "^4.2.11", "hosted-git-info": "^8.0.2", "ini": "^5.0.0", - "init-package-json": "^7.0.1", + "init-package-json": "^7.0.2", "is-cidr": "^5.1.0", "json-parse-even-better-errors": "^4.0.0", "libnpmaccess": "^9.0.0", @@ -10156,7 +10170,7 @@ "minipass": "^7.1.1", "minipass-pipeline": "^1.2.4", "ms": "^2.1.2", - "node-gyp": "^10.2.0", + "node-gyp": "^11.0.0", "nopt": "^8.0.0", "normalize-package-data": "^7.0.0", "npm-audit-report": "^6.0.0", @@ -10409,7 +10423,7 @@ } }, "node_modules/npm/node_modules/@npmcli/map-workspaces": { - "version": "4.0.1", + "version": "4.0.2", "inBundle": true, "license": "ISC", "dependencies": { @@ -10484,7 +10498,7 @@ } }, "node_modules/npm/node_modules/@npmcli/package-json": { - "version": "6.0.1", + "version": "6.1.0", "inBundle": true, "license": "ISC", "dependencies": { @@ -10531,14 +10545,14 @@ } }, "node_modules/npm/node_modules/@npmcli/run-script": { - "version": "9.0.1", + "version": "9.0.2", "inBundle": true, "license": "ISC", "dependencies": { "@npmcli/node-gyp": "^4.0.0", "@npmcli/package-json": "^6.0.0", "@npmcli/promise-spawn": "^8.0.0", - "node-gyp": "^10.0.0", + "node-gyp": "^11.0.0", "proc-log": "^5.0.0", "which": "^5.0.0" }, @@ -11111,7 +11125,7 @@ } }, "node_modules/npm/node_modules/init-package-json": { - "version": "7.0.1", + "version": "7.0.2", "inBundle": true, "license": "ISC", "dependencies": { @@ -11169,11 +11183,6 @@ "node": ">=8" } }, - "node_modules/npm/node_modules/is-lambda": { - "version": "1.0.1", - "inBundle": true, - "license": "MIT" - }, "node_modules/npm/node_modules/isexe": { "version": "2.0.0", "inBundle": true, @@ -11595,16 +11604,8 @@ "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm/node_modules/negotiator": { - "version": "0.6.4", - "inBundle": true, - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, "node_modules/npm/node_modules/node-gyp": { - "version": "10.2.0", + "version": "11.0.0", "inBundle": true, "license": "MIT", "dependencies": { @@ -11612,189 +11613,76 @@ "exponential-backoff": "^3.1.1", "glob": "^10.3.10", "graceful-fs": "^4.2.6", - "make-fetch-happen": "^13.0.0", - "nopt": "^7.0.0", - "proc-log": "^4.1.0", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", "semver": "^7.3.5", - "tar": "^6.2.1", - "which": "^4.0.0" + "tar": "^7.4.3", + "which": "^5.0.0" }, "bin": { "node-gyp": "bin/node-gyp.js" }, "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/@npmcli/agent": { - "version": "2.2.2", - "inBundle": true, - "license": "ISC", - "dependencies": { - "agent-base": "^7.1.0", - "http-proxy-agent": "^7.0.0", - "https-proxy-agent": "^7.0.1", - "lru-cache": "^10.0.1", - "socks-proxy-agent": "^8.0.3" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/@npmcli/fs": { - "version": "3.1.1", - "inBundle": true, - "license": "ISC", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/abbrev": { - "version": "2.0.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/cacache": { - "version": "18.0.4", - "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/fs": "^3.1.0", - "fs-minipass": "^3.0.0", - "glob": "^10.2.2", - "lru-cache": "^10.0.1", - "minipass": "^7.0.3", - "minipass-collect": "^2.0.1", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "p-map": "^4.0.0", - "ssri": "^10.0.0", - "tar": "^6.1.11", - "unique-filename": "^3.0.0" - }, - "engines": { - "node": "^16.14.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/isexe": { - "version": "3.1.1", - "inBundle": true, - "license": "ISC", - "engines": { - "node": ">=16" + "node": "^18.17.0 || >=20.5.0" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/make-fetch-happen": { - "version": "13.0.1", + "node_modules/npm/node_modules/node-gyp/node_modules/chownr": { + "version": "3.0.0", "inBundle": true, - "license": "ISC", - "dependencies": { - "@npmcli/agent": "^2.0.0", - "cacache": "^18.0.0", - "http-cache-semantics": "^4.1.1", - "is-lambda": "^1.0.1", - "minipass": "^7.0.2", - "minipass-fetch": "^3.0.0", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.4", - "negotiator": "^0.6.3", - "proc-log": "^4.2.0", - "promise-retry": "^2.0.1", - "ssri": "^10.0.0" - }, + "license": "BlueOak-1.0.0", "engines": { - "node": "^16.14.0 || >=18.0.0" + "node": ">=18" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/minipass-fetch": { - "version": "3.0.5", + "node_modules/npm/node_modules/node-gyp/node_modules/minizlib": { + "version": "3.0.1", "inBundle": true, "license": "MIT", "dependencies": { - "minipass": "^7.0.3", - "minipass-sized": "^1.0.3", - "minizlib": "^2.1.2" + "minipass": "^7.0.4", + "rimraf": "^5.0.5" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - }, - "optionalDependencies": { - "encoding": "^0.1.13" + "node": ">= 18" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/nopt": { - "version": "7.2.1", + "node_modules/npm/node_modules/node-gyp/node_modules/mkdirp": { + "version": "3.0.1", "inBundle": true, - "license": "ISC", - "dependencies": { - "abbrev": "^2.0.0" - }, + "license": "MIT", "bin": { - "nopt": "bin/nopt.js" - }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/proc-log": { - "version": "4.2.0", - "inBundle": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/ssri": { - "version": "10.0.6", - "inBundle": true, - "license": "ISC", - "dependencies": { - "minipass": "^7.0.3" + "mkdirp": "dist/cjs/src/bin.js" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, - "node_modules/npm/node_modules/node-gyp/node_modules/unique-filename": { - "version": "3.0.0", - "inBundle": true, - "license": "ISC", - "dependencies": { - "unique-slug": "^4.0.0" + "node": ">=10" }, - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/unique-slug": { - "version": "4.0.0", + "node_modules/npm/node_modules/node-gyp/node_modules/tar": { + "version": "7.4.3", "inBundle": true, "license": "ISC", "dependencies": { - "imurmurhash": "^0.1.4" + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" }, "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + "node": ">=18" } }, - "node_modules/npm/node_modules/node-gyp/node_modules/which": { - "version": "4.0.0", + "node_modules/npm/node_modules/node-gyp/node_modules/yallist": { + "version": "5.0.0", "inBundle": true, - "license": "ISC", - "dependencies": { - "isexe": "^3.1.1" - }, - "bin": { - "node-which": "bin/which.js" - }, + "license": "BlueOak-1.0.0", "engines": { - "node": "^16.13.0 || >=18.0.0" + "node": ">=18" } }, "node_modules/npm/node_modules/nopt": { @@ -12769,14 +12657,16 @@ } }, "node_modules/object.assign": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.5.tgz", - "integrity": "sha512-byy+U7gp+FVwmyzKPYhW2h5l3crpmGsxl7X2s8y43IgxvG4g3QZ6CffDtsNQy1WsmZpQbO+ybo0AlW7TY6DcBQ==", + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", "dev": true, "dependencies": { - "call-bind": "^1.0.5", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", - "has-symbols": "^1.0.3", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", "object-keys": "^1.1.1" }, "engines": { @@ -12833,12 +12723,13 @@ } }, "node_modules/object.values": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.0.tgz", - "integrity": "sha512-yBYjY9QX2hnRmZHAjG/f13MzmBzxzYgQhFrke06TTyKY5zSTEqkOeukBzIdVA3j3ulu8Qa3MbVFShV7T2RmGtQ==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, @@ -12892,14 +12783,14 @@ } }, "node_modules/oniguruma-to-es": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-0.4.1.tgz", - "integrity": "sha512-rNcEohFz095QKGRovP/yqPIKc+nP+Sjs4YTHMv33nMePGKrq/r2eu9Yh4646M5XluGJsUnmwoXuiXE69KDs+fQ==", + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-0.8.0.tgz", + "integrity": "sha512-rY+/a6b+uCgoYIL9itjY0x99UUDHXmGaw7Jjk5ZvM/3cxDJifyxFr/Zm4tTmF6Tre18gAakJo7AzhKUeMNLgHA==", "dev": true, "dependencies": { "emoji-regex-xs": "^1.0.0", - "regex": "^5.0.0", - "regex-recursion": "^4.2.1" + "regex": "^5.0.2", + "regex-recursion": "^5.0.0" } }, "node_modules/openapi-types": { @@ -13125,9 +13016,9 @@ "dev": true }, "node_modules/path-to-regexp": { - "version": "0.1.10", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.10.tgz", - "integrity": "sha512-7lf7qcQidTku0Gu3YDPc8DJ1q7OOucfa/BSsIwjuh56VU7katFvuM8hULfkwB3Fns/rsVF7PwPKVw1sl5KQS9w==" + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==" }, "node_modules/path-type": { "version": "4.0.0", @@ -13218,9 +13109,9 @@ } }, "node_modules/prettier": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.3.3.tgz", - "integrity": "sha512-i2tDNA0O5IrMO757lfrdQZCc2jPNDVntV0m/+4whiDfWaTKfMNgR7Qz0NAeGz/nRqF4m5/6CLzbP4/liHt12Ew==", + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.4.2.tgz", + "integrity": "sha512-e9MewbtFo+Fevyuxn/4rrcDAaq0IYxPGLvObpQjiZBMAzB9IGmzlnG9RZy3FFas+eBMu2vA0CszMeduow5dIuQ==", "dev": true, "bin": { "prettier": "bin/prettier.cjs" @@ -13333,11 +13224,14 @@ "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==" }, "node_modules/psl": { - "version": "1.10.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.10.0.tgz", - "integrity": "sha512-KSKHEbjAnpUuAUserOq0FxGXCUrzC3WniuSJhvdbs102rL55266ZcHBqLWOsG30spQMlPdpy7icATiAQehg/iA==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.15.0.tgz", + "integrity": "sha512-JZd3gMVBAVQkSs6HdNZo9Sdo0LNcQeMNP3CozBJb3JYC/QUYZTnKxP+f8oWRX4rHP5EurWxqAHTSwUCjlNKa1w==", "dependencies": { "punycode": "^2.3.1" + }, + "funding": { + "url": "https://github.com/sponsors/lupomontero" } }, "node_modules/pstree.remy": { @@ -13493,9 +13387,6 @@ "version": "4.7.0", "resolved": "https://registry.npmjs.org/redis/-/redis-4.7.0.tgz", "integrity": "sha512-zvmkHEAdGMn+hMRXuMBtu4Vo5P6rHQjLoHftu+lBqq8ZTA3RCVC/WzD790bkKKiNFp7d5/9PcSD19fJyyRvOdQ==", - "workspaces": [ - "./packages/*" - ], "dependencies": { "@redis/bloom": "1.2.0", "@redis/client": "1.6.0", @@ -13506,18 +13397,19 @@ } }, "node_modules/reflect.getprototypeof": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.6.tgz", - "integrity": "sha512-fmfw4XgoDke3kdI6h4xcUz1dG8uaiv5q9gcEwLS4Pnth2kxT+GZ7YehS1JTMGBQmtV7Y4GFGbs2re2NqhdozUg==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.9.tgz", + "integrity": "sha512-r0Ay04Snci87djAsI4U+WNRcSw5S4pOH7qFjd/veA5gC7TbqESR3tcj28ia95L/fYUDw11JKP7uqUKUAfVvV5Q==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "define-properties": "^1.2.1", - "es-abstract": "^1.23.1", + "dunder-proto": "^1.0.1", + "es-abstract": "^1.23.6", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "globalthis": "^1.0.3", - "which-builtin-type": "^1.1.3" + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "which-builtin-type": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -13569,9 +13461,9 @@ } }, "node_modules/regex-recursion": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-4.2.1.tgz", - "integrity": "sha512-QHNZyZAeKdndD1G3bKAbBEKOSSK4KOHQrAJ01N1LJeb0SoH4DJIeFhp0uUpETgONifS4+P3sOgoA1dhzgrQvhA==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/regex-recursion/-/regex-recursion-5.0.0.tgz", + "integrity": "sha512-UwyOqeobrCCqTXPcsSqH4gDhOjD5cI/b8kjngWgSZbxYh5yVjAwTjO5+hAuPRNiuR70+5RlWSs+U9PVcVcW9Lw==", "dev": true, "dependencies": { "regex-utilities": "^2.3.0" @@ -13614,15 +13506,15 @@ } }, "node_modules/regexpu-core": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.1.1.tgz", - "integrity": "sha512-k67Nb9jvwJcJmVpw0jPttR1/zVfnKf8Km0IPatrU/zJ5XeG3+Slx0xLXs9HByJSzXzrlz5EDvN6yLNMDc2qdnw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", + "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", "dev": true, "dependencies": { "regenerate": "^1.4.2", "regenerate-unicode-properties": "^10.2.0", "regjsgen": "^0.8.0", - "regjsparser": "^0.11.0", + "regjsparser": "^0.12.0", "unicode-match-property-ecmascript": "^2.0.0", "unicode-match-property-value-ecmascript": "^2.1.0" }, @@ -13637,9 +13529,9 @@ "dev": true }, "node_modules/regjsparser": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.11.2.tgz", - "integrity": "sha512-3OGZZ4HoLJkkAZx/48mTXJNlmqTGOzc0o9OWQPuWpkOlXXPbyN6OafCcoXUnBqE2D3f/T5L+pWc1kdEmnfnRsA==", + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", "dev": true, "dependencies": { "jsesc": "~3.0.2" @@ -13648,6 +13540,18 @@ "regjsparser": "bin/parser" } }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/request": { "version": "2.88.2", "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", @@ -13710,18 +13614,21 @@ } }, "node_modules/resolve": { - "version": "1.22.8", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", - "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", "dev": true, "dependencies": { - "is-core-module": "^2.13.0", + "is-core-module": "^2.16.0", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -13757,9 +13664,9 @@ } }, "node_modules/resolve.exports": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", - "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", "dev": true, "engines": { "node": ">=10" @@ -13823,14 +13730,15 @@ } }, "node_modules/safe-array-concat": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.2.tgz", - "integrity": "sha512-vj6RsCsWBCf19jIeHEfkRMw8DPiBb+DMXklQ/1SGDHOMlHdPUkZXFQ2YdplS23zESTijAcurb1aSgJA3AgMu1Q==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", - "get-intrinsic": "^1.2.4", - "has-symbols": "^1.0.3", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", "isarray": "^2.0.5" }, "engines": { @@ -13866,14 +13774,14 @@ ] }, "node_modules/safe-regex-test": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.0.3.tgz", - "integrity": "sha512-CdASjNJPvRa7roO6Ra/gLYBTzYzzPyyBXxIMdGW3USQLyjWEls2RgW5UBTXaQVp+OrpeCK3bLem8smtmheoRuw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, "dependencies": { - "call-bind": "^1.0.6", + "call-bound": "^1.0.2", "es-errors": "^1.3.0", - "is-regex": "^1.1.4" + "is-regex": "^1.2.1" }, "engines": { "node": ">= 0.4" @@ -13962,6 +13870,7 @@ "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dev": true, "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", @@ -14016,37 +13925,91 @@ } }, "node_modules/shell-quote": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", - "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.2.tgz", + "integrity": "sha512-AzqKpGKjrj7EM6rKVQEPpB288oCfnrEIuyoT9cyF4nmGa7V8Zk6f7RRqYisX8X9m+Q7bd632aZW4ky7EhbQztA==", "dev": true, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/shiki": { - "version": "1.23.1", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.23.1.tgz", - "integrity": "sha512-8kxV9TH4pXgdKGxNOkrSMydn1Xf6It8lsle0fiqxf7a1149K1WGtdOu3Zb91T5r1JpvRPxqxU3C2XdZZXQnrig==", + "version": "1.24.3", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-1.24.3.tgz", + "integrity": "sha512-eMeX/ehE2IDKVs71kB4zVcDHjutNcOtm+yIRuR4sA6ThBbdFI0DffGJiyoKCodj0xRGxIoWC3pk/Anmm5mzHmA==", "dev": true, "dependencies": { - "@shikijs/core": "1.23.1", - "@shikijs/engine-javascript": "1.23.1", - "@shikijs/engine-oniguruma": "1.23.1", - "@shikijs/types": "1.23.1", - "@shikijs/vscode-textmate": "^9.3.0", + "@shikijs/core": "1.24.3", + "@shikijs/engine-javascript": "1.24.3", + "@shikijs/engine-oniguruma": "1.24.3", + "@shikijs/types": "1.24.3", + "@shikijs/vscode-textmate": "^9.3.1", "@types/hast": "^3.0.4" } }, "node_modules/side-channel": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.6.tgz", - "integrity": "sha512-fDW/EZ6Q9RiO8eFG8Hj+7u/oW+XrPTIChwCOM2+th2A6OblDtYYIpve9m+KvI9Z4C9qSEXlaGR6bTEYHReuglA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dependencies": { - "call-bind": "^1.0.7", "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.4", - "object-inspect": "^1.13.1" + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" }, "engines": { "node": ">= 0.4" @@ -14108,7 +14071,6 @@ "version": "4.8.1", "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", - "license": "MIT", "dependencies": { "accepts": "~1.3.4", "base64id": "~2.0.0", @@ -14131,6 +14093,22 @@ "ws": "~8.17.1" } }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/socket.io-parser": { "version": "4.2.4", "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", @@ -14143,6 +14121,38 @@ "node": ">=10.0.0" } }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -14209,6 +14219,12 @@ "node": ">=0.10.0" } }, + "node_modules/stable-hash": { + "version": "0.0.4", + "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.4.tgz", + "integrity": "sha512-LjdcbuBeLcdETCrPn9i8AYAZ1eCtu4ECAWtP7UleOiZ9LzVxRzzUZEoZ8zB24nhkQnDWyET0I+3sWokSDS3E7g==", + "dev": true + }, "node_modules/stack-utils": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", @@ -14341,15 +14357,18 @@ } }, "node_modules/string.prototype.trim": { - "version": "1.2.9", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.9.tgz", - "integrity": "sha512-klHuCNxiMZ8MlsOihJhJEBJAiMVqU3Z2nEXWfWnIqjN0gEFS9J9+IxKozWWtQGcgoa1WUZzLjKPTr4ZHNFTFxw==", + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", "define-properties": "^1.2.1", - "es-abstract": "^1.23.0", - "es-object-atoms": "^1.0.0" + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" }, "engines": { "node": ">= 0.4" @@ -14359,15 +14378,19 @@ } }, "node_modules/string.prototype.trimend": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.8.tgz", - "integrity": "sha512-p73uL5VCHCO2BZZ6krwwQE3kCzM7NKmis8S//xEC6fQonchbum4eP6kR4DLEjQFO3Wnj3Fuo8NM0kOSjVdHjZQ==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" }, + "engines": { + "node": ">= 0.4" + }, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -14490,7 +14513,6 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -14668,8 +14690,47 @@ "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, "node_modules/telware-backend": { - "resolved": "", - "link": true + "version": "1.0.0", + "resolved": "file:", + "license": "ISC", + "dependencies": { + "@faker-js/faker": "^9.0.3", + "axios": "^1.7.7", + "bcrypt": "^5.1.1", + "body-parser": "^1.20.3", + "connect-redis": "^7.1.1", + "cors": "^2.8.5", + "crypto": "^1.0.1", + "dotenv": "^16.4.5", + "express": "^4.21.1", + "express-mongo-sanitize": "^2.2.0", + "express-rate-limit": "^7.4.1", + "express-session": "^1.18.1", + "helmet": "^8.0.0", + "hpp": "^0.2.3", + "jsonwebtoken": "^9.0.2", + "module-alias": "^2.2.3", + "mongodb": "^6.11.0", + "mongoose": "^8.7.2", + "morgan": "^1.10.0", + "multer": "^1.4.5-lts.1", + "node-fetch": "^3.3.2", + "nodemailer": "^6.9.15", + "npm": "^10.9.1", + "passport": "^0.7.0", + "passport-github2": "^0.1.12", + "passport-google-oauth20": "^2.0.0", + "redis": "^4.7.0", + "request": "^2.88.2", + "socket.io": "^4.8.1", + "supertest": "^7.0.0", + "swagger-jsdoc": "^6.2.8", + "swagger-ui-express": "^5.0.1", + "telware-backend": "file:", + "ua-parser-js": "^1.0.39", + "validator": "^13.12.0", + "yamljs": "^0.3.0" + } }, "node_modules/test-exclude": { "version": "6.0.0", @@ -14710,8 +14771,7 @@ "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", - "dev": true + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" }, "node_modules/tmpl": { "version": "1.0.5", @@ -14791,9 +14851,9 @@ } }, "node_modules/ts-api-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.0.tgz", - "integrity": "sha512-032cPxaEKwM+GT3vA5JXNzIaizx388rhsSW79vGRNGXfRRAdEAn2mvk36PvK5HnOchyWZ7afLEXqYCvPCrzuzQ==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", "dev": true, "engines": { "node": ">=16" @@ -15121,30 +15181,30 @@ } }, "node_modules/typed-array-buffer": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.2.tgz", - "integrity": "sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", + "call-bound": "^1.0.3", "es-errors": "^1.3.0", - "is-typed-array": "^1.1.13" + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" } }, "node_modules/typed-array-byte-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.1.tgz", - "integrity": "sha512-3iMJ9q0ao7WE9tWcaYKIptkNBuOIcZCCT0d4MRvuuH88fEoEH62IuQe0OtraD3ebQEoTRk8XCBoknUNc1Y67pw==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", "dev": true, "dependencies": { - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" }, "engines": { "node": ">= 0.4" @@ -15154,17 +15214,18 @@ } }, "node_modules/typed-array-byte-offset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.2.tgz", - "integrity": "sha512-Ous0vodHa56FviZucS2E63zkgtgrACj7omjwd/8lTEMEPFFyjfixMZ1ZXenpgCFBBt4EC1J2XsyVS2gkG0eTFA==", + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", "dev": true, "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", "for-each": "^0.3.3", - "gopd": "^1.0.1", - "has-proto": "^1.0.3", - "is-typed-array": "^1.1.13" + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" }, "engines": { "node": ">= 0.4" @@ -15174,17 +15235,17 @@ } }, "node_modules/typed-array-length": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.6.tgz", - "integrity": "sha512-/OxDN6OtAk5KBpGb28T+HZc2M+ADtvRxXrKKbUwtsLgdoxgX13hyy7ek6bFRl5+aBs2yZzB0c4CnQfAtVypW/g==", + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", "dev": true, "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", - "has-proto": "^1.0.3", "is-typed-array": "^1.1.13", - "possible-typed-array-names": "^1.0.0" + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" }, "engines": { "node": ">= 0.4" @@ -15246,14 +15307,14 @@ } }, "node_modules/typescript-eslint": { - "version": "8.13.0", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.13.0.tgz", - "integrity": "sha512-vIMpDRJrQd70au2G8w34mPps0ezFSPMEX4pXkTzUkrNbRX+36ais2ksGWN0esZL+ZMaFJEneOBHzCgSqle7DHw==", + "version": "8.18.1", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.18.1.tgz", + "integrity": "sha512-Mlaw6yxuaDEPQvb/2Qwu3/TfgeBHy9iTJ3mTwe7OvpPmF6KPQjVOfGyEJpPv6Ez2C34OODChhXrzYw/9phI0MQ==", "dev": true, "dependencies": { - "@typescript-eslint/eslint-plugin": "8.13.0", - "@typescript-eslint/parser": "8.13.0", - "@typescript-eslint/utils": "8.13.0" + "@typescript-eslint/eslint-plugin": "8.18.1", + "@typescript-eslint/parser": "8.18.1", + "@typescript-eslint/utils": "8.18.1" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -15262,10 +15323,9 @@ "type": "opencollective", "url": "https://opencollective.com/typescript-eslint" }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <5.8.0" } }, "node_modules/ua-parser-js": { @@ -15316,15 +15376,18 @@ "integrity": "sha512-IevTus0SbGwQzYh3+fRsAMTVVPOoIVufzacXcHPmdlle1jUpq7BRL+mw3dgeLanvGZdwwbWhRV6XrcFNdBmjWA==" }, "node_modules/unbox-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.2.tgz", - "integrity": "sha512-61pPlCD9h51VoreyJ0BReideM3MDKMKnh6+V9L08331ipq6Q8OFXZYiqP6n/tbHx4s5I9uRhcye6BrbkizkBDw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", "dev": true, "dependencies": { - "call-bind": "^1.0.2", + "call-bound": "^1.0.3", "has-bigints": "^1.0.2", - "has-symbols": "^1.0.3", - "which-boxed-primitive": "^1.0.2" + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" @@ -15337,9 +15400,9 @@ "dev": true }, "node_modules/undici-types": { - "version": "6.19.8", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.19.8.tgz", - "integrity": "sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw==" + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==" }, "node_modules/unicode-canonical-property-names-ecmascript": { "version": "2.0.1", @@ -15649,7 +15712,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -15661,39 +15723,43 @@ } }, "node_modules/which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", "dev": true, "dependencies": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" }, "funding": { "url": "https://github.com/sponsors/ljharb" } }, "node_modules/which-builtin-type": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.1.4.tgz", - "integrity": "sha512-bppkmBSsHFmIMSl8BO9TbsyzsvGjVoppt8xUiGzwiu/bhDCGxnpOKCxgqj6GuyHE0mINMDecBFPlOm2hzY084w==", + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", "dev": true, "dependencies": { + "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", - "is-date-object": "^1.0.5", - "is-finalizationregistry": "^1.0.2", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", - "is-regex": "^1.1.4", + "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", - "which-boxed-primitive": "^1.0.2", + "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", - "which-typed-array": "^1.1.15" + "which-typed-array": "^1.1.16" }, "engines": { "node": ">= 0.4" @@ -15727,15 +15793,16 @@ } }, "node_modules/which-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.15.tgz", - "integrity": "sha512-oV0jmFtUky6CXfkqehVvBP/LSWJ2sy4vWMioiENyJLePrBO/yKyV9OyJySfAKosh+RYkIl5zJCNZ8/4JncrpdA==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.18.tgz", + "integrity": "sha512-qEcY+KJYlWyLH9vNbsr6/5j59AXk5ni5aakf8ldzBvGde6Iz4sxZGkJyWSAueTG7QhOvNRYb1lDdFmL5Td0QKA==", "dev": true, "dependencies": { "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", "for-each": "^0.3.3", - "gopd": "^1.0.1", + "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" }, "engines": { @@ -15788,7 +15855,6 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", - "dev": true, "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" diff --git a/src/sockets/chats.ts b/src/sockets/chats.ts index 597736f..ffc2967 100644 --- a/src/sockets/chats.ts +++ b/src/sockets/chats.ts @@ -27,7 +27,7 @@ const handleAddAdmins = async ( chatType: ['group', 'channel'], checkAdmin: true, }); - if (func) return func; + if (!func) return func; await Promise.all( members.map(async (memId: string) => { @@ -87,7 +87,7 @@ const handleAddMembers = async ( chatType: ['group', 'channel'], checkAdmin: true, }); - if (func) return func; + if (!func) return func; if ( chat?.type === 'group' && diff --git a/src/sockets/messages.ts b/src/sockets/messages.ts index a842f33..f5d617a 100644 --- a/src/sockets/messages.ts +++ b/src/sockets/messages.ts @@ -44,7 +44,7 @@ const handleMessaging = async ( const func = await check(chat, ack, senderId, { newMessageIsReply: isReply, }); - if (func) return func; + if (!func) return; let parentMessage; if (isForward || isReply) { From 38b7f35d213c9bcbd7d07fce2bdb8c4bbe7d5e72 Mon Sep 17 00:00:00 2001 From: Peter Safwat <118059454+PeterSafwatHBK@users.noreply.github.com> Date: Fri, 20 Dec 2024 16:59:35 +0200 Subject: [PATCH 33/54] feat: created admin dashboard --- src/controllers/authController.ts | 11 +++++ src/controllers/userController.ts | 74 +++++++++++++++++++++++++++++++ src/database/seed/json/users.json | 15 ++++++- src/errors/errorHandlers.ts | 4 ++ src/errors/globalErrorHandler.ts | 7 +++ src/middlewares/authMiddleware.ts | 14 ++++++ src/routes/authRoute.ts | 5 ++- src/routes/userRoute.ts | 18 ++++++-- 8 files changed, 141 insertions(+), 7 deletions(-) diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index 0043ebe..86d50ef 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -71,18 +71,29 @@ export const signup = catchAsync( export const login = catchAsync( async (req: Request, res: Response, next: NextFunction) => { const { email, password } = req.body; + const user = await User.findOne({ email }); if (!user) return next( new AppError('No user is found with this email address', 404) ); + if (user.accountStatus !== 'active') { + return next( + new AppError( + 'Your account is deactivated or banned.', + 403 + ) + ); + } + const message: string = await validateBeforeLogin(email, password); if (message === 'please verify your email first to be able to login') return next(new AppError(message, 403)); if (message !== 'validated') return next(new AppError(message, 400)); await saveSession(user._id as ObjectId, req); + res.status(200).json({ status: 'success', message: 'logged in successfully', diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts index 2c7040c..fca94a6 100644 --- a/src/controllers/userController.ts +++ b/src/controllers/userController.ts @@ -4,6 +4,7 @@ import catchAsync from '@base/utils/catchAsync'; import { Request, Response } from 'express'; import deletePictureFile from '@base/services/userService'; import IUser from '@base/types/user'; +import GroupChannel from '@base/models/groupChannelModel'; interface GetUser extends Request { params: { @@ -262,3 +263,76 @@ export const deletePicture = catchAsync(async (req: any, res: Response) => { data: {}, }); }); +export const getAllGroups = catchAsync(async (req: Request, res: Response) => { + const groupsAndChannels = await GroupChannel.find(); // Use `find()` in Mongoose to retrieve all documents + console.log(groupsAndChannels) + return res.status(200).json({ + status: 'success', + message: 'Groups and Channels retrieved successfully', + data: { + groupsAndChannels, + }, + }); +}); +export const activateUser = catchAsync(async (req: Request, res: Response) => { + const userId = req.params.userId; + + const user = await User.findById(userId); + if (!user) { + return res.status(404).json({ + status: 'fail', + message: 'User not found', + }); + } +if (user.accountStatus === 'banned') { + return res.status(400).json({ + status: 'fail', + message: 'User is Banned', + }); + } + user.accountStatus = 'active'; + await user.save(); + + return res.status(200).json({ + status: 'success', + message: 'User activated successfully', + }); +}); +export const deactivateUser = catchAsync(async (req: Request, res: Response) => { + const userId = req.params.userId; + console.log(req.params) + const user = await User.findById(userId); + if (!user) { + return res.status(404).json({ + status: 'fail', + message: 'User not found', + }); + } + + user.accountStatus = 'deactivated'; + await user.save(); + + return res.status(200).json({ + status: 'success', + message: 'User deactivated successfully', + }); +}); +export const banUser = catchAsync(async (req: Request, res: Response) => { + const userId = req.params.userId; + + const user = await User.findById(userId); + if (!user) { + return res.status(404).json({ + status: 'fail', + message: 'User not found', + }); + } + + user.accountStatus = 'banned'; + await user.save(); + + return res.status(200).json({ + status: 'success', + message: 'User banned successfully', + }); +}); \ No newline at end of file diff --git a/src/database/seed/json/users.json b/src/database/seed/json/users.json index 478e3b7..0ab2e2c 100644 --- a/src/database/seed/json/users.json +++ b/src/database/seed/json/users.json @@ -1,4 +1,15 @@ [ + { + "email": "admin@gmail.com", + "username": "Hamdy", + "screenFirstName": "Ahmed", + "screenLastName": "Hamdy", + "phoneNumber": "+201063360716", + "password": "12345678", + "passwordConfirm": "12345678", + "accountStatus": "active", + "isAdmin": true + }, { "email": "front1@gmail.com", "username": "Batman", @@ -17,7 +28,7 @@ "phoneNumber": "+201055445511", "password": "12345678", "passwordConfirm": "12345678", - "accountStatus": "active" + "accountStatus": "deactivated" }, { "email": "front3@gmail.com", @@ -27,7 +38,7 @@ "phoneNumber": "+201055287611", "password": "12345678", "passwordConfirm": "12345678", - "accountStatus": "active" + "accountStatus": "banned" }, { "email": "front4@gmail.com", diff --git a/src/errors/errorHandlers.ts b/src/errors/errorHandlers.ts index 68060e8..ce37ee7 100644 --- a/src/errors/errorHandlers.ts +++ b/src/errors/errorHandlers.ts @@ -32,3 +32,7 @@ export const handleInvalidPrivacyOption = (err: AppError) => { err.message = 'Invalid Privacy Option.'; return new AppError(err.message, 400); }; +export const handleInvalidAuth = (err: AppError) => { + err.message = 'You are not an Admin.'; + return new AppError(err.message, 400); +}; diff --git a/src/errors/globalErrorHandler.ts b/src/errors/globalErrorHandler.ts index e406cc4..2e2f793 100644 --- a/src/errors/globalErrorHandler.ts +++ b/src/errors/globalErrorHandler.ts @@ -5,6 +5,7 @@ import { sendDevError, sendProdError, handleInvalidPrivacyOption, + handleInvalidAuth } from './errorHandlers'; const globalErrorHandler = ( @@ -30,6 +31,12 @@ const globalErrorHandler = ( sendProdError(err, res); } + if ( + err.message === + "You are not authorized to access this resource" + ) + err = handleInvalidAuth(err); + }; export default globalErrorHandler; diff --git a/src/middlewares/authMiddleware.ts b/src/middlewares/authMiddleware.ts index 2ed078c..5da0901 100644 --- a/src/middlewares/authMiddleware.ts +++ b/src/middlewares/authMiddleware.ts @@ -4,6 +4,7 @@ import AppError from '@errors/AppError'; import User from '@models/userModel'; import { reloadSession } from '@services/sessionService'; import redisClient from '@base/config/redis'; +import IUser from '@base/types/user'; export const protect = catchAsync( async (req: Request, res: Response, next: NextFunction) => { @@ -45,6 +46,19 @@ export const savePlatformInfo = catchAsync( export const isAdmin = catchAsync( async (req: Request, res: Response, next: NextFunction) => { + const currentUser = req.user as IUser; + if (!currentUser || !currentUser.isAdmin) { + return next(new AppError('You are not authorized to access this resource', 403)); + } + next(); + } +); +export const isActive = catchAsync( + async (req: Request, res: Response, next: NextFunction) => { + const currentUser = req.user as IUser; + if (!currentUser || currentUser.accountStatus !== 'active') { + return next(new AppError('You are not active', 403)); + } next(); } ); diff --git a/src/routes/authRoute.ts b/src/routes/authRoute.ts index d14ffe8..f840376 100644 --- a/src/routes/authRoute.ts +++ b/src/routes/authRoute.ts @@ -13,7 +13,7 @@ import { getLogedInSessions, getCurrentSession, } from '@controllers/authController'; -import { protect } from '@middlewares/authMiddleware'; +import { protect , isActive } from '@middlewares/authMiddleware'; import oauthRouter from '@base/routes/oauthRoute'; const router = Router(); @@ -27,10 +27,11 @@ router.post('/send-confirmation', sendConfirmationCode); router.post('/verify', verifyEmail); router.post('/password/forget', forgotPassword); router.patch('/password/reset/:token', resetPassword); -router.patch('/password/change', protect, changePassword); router.use(protect); +router.patch('/password/change', protect, changePassword); +router.use(isActive); router.get('/me', getCurrentSession); router.get('/sessions', getLogedInSessions); router.post('/logout', logoutSession); diff --git a/src/routes/userRoute.ts b/src/routes/userRoute.ts index efc027b..2d610a1 100644 --- a/src/routes/userRoute.ts +++ b/src/routes/userRoute.ts @@ -19,6 +19,10 @@ import { updatePicture, updateScreenName, updateUsername, + getAllGroups, + activateUser, + deactivateUser, + banUser } from '@controllers/userController'; import { deleteStory, @@ -27,11 +31,13 @@ import { getStory, postStory, } from '@controllers/storyController'; -import { protect } from '@middlewares/authMiddleware'; +import { protect,isAdmin,isActive } from '@middlewares/authMiddleware'; + const router = Router(); router.use(protect); +router.use(isActive); router.use('/privacy', privacyRouter); router.get('/stories', getCurrentUserStory); router.post('/stories', upload.single('file'), postStory); @@ -42,8 +48,13 @@ router.get('/block', getBlockedUsers); router.post('/block/:id', block); router.delete('/block/:id', unblock); +// Admin routes +router.patch('/activate/:userId',isAdmin,activateUser); +router.patch('/deactivate/:userId',isAdmin,deactivateUser); +router.patch('/ban/:userId',isAdmin,banUser); +router.get('/all-groups',isAdmin,getAllGroups); + // User routes -router.get('/', getAllUsers); router.get('/me', getCurrentUser); router.get('/username/check', getCheckUserName); router.patch('/me', updateCurrentUser); @@ -55,7 +66,8 @@ router.patch('/screen-name', updateScreenName); router.patch('/picture', upload.single('file'), updatePicture); router.delete('/picture', deletePicture); router.get('/contacts/stories', getAllContactsStories); -router.get('/:userId', getUser); router.get('/:userId/stories', getStory); +router.get('/:userId', getUser); +router.get('/',isAdmin, getAllUsers); export default router; From 8301805d43a1af659d174e268c5378f85e66cdb6 Mon Sep 17 00:00:00 2001 From: Peter Safwat <118059454+PeterSafwatHBK@users.noreply.github.com> Date: Fri, 20 Dec 2024 17:04:11 +0200 Subject: [PATCH 34/54] feat: AI Integration --- package-lock.json | 29 ++++++++++++++++++++++++- package.json | 4 +++- src/controllers/authController.ts | 3 +++ src/controllers/chatController.ts | 24 +++++++++++++++++++++ src/errors/globalErrorHandler.ts | 5 +++++ src/models/messageModel.ts | 4 ++++ src/routes/chatRoute.ts | 3 +++ src/routes/userRoute.ts | 2 +- src/services/googleAIService.ts | 35 +++++++++++++++++++++++++++++++ src/sockets/messages.ts | 7 +++++++ src/types/message.ts | 3 ++- 11 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 src/services/googleAIService.ts diff --git a/package-lock.json b/package-lock.json index 32dea14..2dc05fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,9 @@ "license": "ISC", "dependencies": { "@faker-js/faker": "^9.0.3", - "axios": "^1.7.7", + "@google/generative-ai": "^0.21.0", + "@huggingface/inference": "^2.8.1", + "axios": "^1.7.9", "bcrypt": "^5.1.1", "body-parser": "^1.20.3", "connect-redis": "^7.1.1", @@ -2405,6 +2407,31 @@ "npm": ">=9.0.0" } }, + "node_modules/@google/generative-ai": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.21.0.tgz", + "integrity": "sha512-7XhUbtnlkSEZK15kN3t+tzIMxsbKm/dSkKBFalj+20NvPKe1kBY7mR2P7vuijEn+f06z5+A8bVGKO0v39cr6Wg==", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@huggingface/inference": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/@huggingface/inference/-/inference-2.8.1.tgz", + "integrity": "sha512-EfsNtY9OR6JCNaUa5bZu2mrs48iqeTz0Gutwf+fU0Kypx33xFQB4DKMhp8u4Ee6qVbLbNWvTHuWwlppLQl4p4Q==", + "license": "MIT", + "dependencies": { + "@huggingface/tasks": "^0.12.9" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@huggingface/tasks": { + "version": "0.12.30", + "resolved": "https://registry.npmjs.org/@huggingface/tasks/-/tasks-0.12.30.tgz", + "integrity": "sha512-A1ITdxbEzx9L8wKR8pF7swyrTLxWNDFIGDLUWInxvks2ruQ8PLRBZe8r0EcjC3CDdtlj9jV1V4cgV35K/iy3GQ==" + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", diff --git a/package.json b/package.json index ddc1441..5053f44 100644 --- a/package.json +++ b/package.json @@ -79,7 +79,9 @@ }, "dependencies": { "@faker-js/faker": "^9.0.3", - "axios": "^1.7.7", + "@google/generative-ai": "^0.21.0", + "@huggingface/inference": "^2.8.1", + "axios": "^1.7.9", "bcrypt": "^5.1.1", "body-parser": "^1.20.3", "connect-redis": "^7.1.1", diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index 86d50ef..0400952 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -379,3 +379,6 @@ export const getCurrentSession = catchAsync( }); } ); + + + diff --git a/src/controllers/chatController.ts b/src/controllers/chatController.ts index 3c2bfe9..16da07f 100644 --- a/src/controllers/chatController.ts +++ b/src/controllers/chatController.ts @@ -331,3 +331,27 @@ export const getVoiceCallsInChat = catchAsync( }); } ); +export const filterChatGroups = catchAsync( + async (req: Request, res: Response, next: NextFunction) => { + const { chatId } = req.params; + + const groupChannel = await GroupChannel.findOneAndUpdate( + { chatId }, + { isFiltered: true }, + { new: true } + ); + + if (!groupChannel) { + return res.status(404).json({ + status: 'fail', + message: 'GroupChannel not found with the given chatId', + }); + } + + res.status(200).json({ + status: 'success', + message: 'isFiltered set to true successfully', + data: groupChannel, + }); + } +); diff --git a/src/errors/globalErrorHandler.ts b/src/errors/globalErrorHandler.ts index 2e2f793..d0d3134 100644 --- a/src/errors/globalErrorHandler.ts +++ b/src/errors/globalErrorHandler.ts @@ -26,6 +26,11 @@ const globalErrorHandler = ( 'Validation failed: invitePermessionsPrivacy: `nobody` is not a valid enum value for path `invitePermessionsPrivacy`.' ) err = handleInvalidPrivacyOption(err); + if ( + err.message === + "You are not authorized to access this resource" + ) + err = handleInvalidAuth(err); if (err.name === 'ValidationError') err = handleDuplicateKeysError(err); diff --git a/src/models/messageModel.ts b/src/models/messageModel.ts index d1da292..5ad101a 100644 --- a/src/models/messageModel.ts +++ b/src/models/messageModel.ts @@ -14,6 +14,10 @@ const messageSchema = new mongoose.Schema({ type: Boolean, default: false, }, + isAppropriate: { + type: Boolean, + default: false, + }, isForward: { type: Boolean, default: false, diff --git a/src/routes/chatRoute.ts b/src/routes/chatRoute.ts index 3743531..af928ba 100644 --- a/src/routes/chatRoute.ts +++ b/src/routes/chatRoute.ts @@ -14,6 +14,7 @@ import { invite, join, getVoiceCallsInChat, + filterChatGroups } from '@base/controllers/chatController'; import { protect } from '@base/middlewares/authMiddleware'; import upload from '@base/config/fileUploads'; @@ -45,4 +46,6 @@ router.get('/messages/:chatId', restrictTo(), getMessages); router.get('/members/:chatId', restrictTo(), getChatMembers); router.get('/:chatId', restrictTo(), getChat); +router.patch('/groups/filter', restrictTo('admin'), filterChatGroups); + export default router; diff --git a/src/routes/userRoute.ts b/src/routes/userRoute.ts index 2d610a1..1e0745f 100644 --- a/src/routes/userRoute.ts +++ b/src/routes/userRoute.ts @@ -33,7 +33,6 @@ import { } from '@controllers/storyController'; import { protect,isAdmin,isActive } from '@middlewares/authMiddleware'; - const router = Router(); router.use(protect); @@ -70,4 +69,5 @@ router.get('/:userId/stories', getStory); router.get('/:userId', getUser); router.get('/',isAdmin, getAllUsers); + export default router; diff --git a/src/services/googleAIService.ts b/src/services/googleAIService.ts new file mode 100644 index 0000000..4f19eb3 --- /dev/null +++ b/src/services/googleAIService.ts @@ -0,0 +1,35 @@ +const { HfInference } = require('@huggingface/inference'); + +const hf = new HfInference(process.env.HF_API_KEY); + +const modelName = 'unitary/toxic-bert'; + +export async function detectInappropriateContent(text: string): Promise { + try { + const response = await hf.textClassification({ + model: modelName, + inputs: text, + }); + + console.log('Model Response:', JSON.stringify(response, null, 2)); + + const relevantLabels = ['toxic', 'obscene', 'insult', 'severe_toxic']; + const threshold = 0.7; + + interface TextClassificationResult { + label: string; + score: number; + } + + const toxicityScore = (response as TextClassificationResult[]) + .filter((result) => relevantLabels.includes(result.label.toLowerCase()) && result.score > threshold) + .reduce((acc, curr) => acc + curr.score, 0); + + console.log(`Total Toxicity Score: ${toxicityScore}`); + + return toxicityScore < threshold; + } catch (error) { + console.error('Error detecting inappropriate content:', error); + throw new Error('Failed to detect inappropriate content'); + } +} \ No newline at end of file diff --git a/src/sockets/messages.ts b/src/sockets/messages.ts index f5d617a..be9147b 100644 --- a/src/sockets/messages.ts +++ b/src/sockets/messages.ts @@ -20,6 +20,7 @@ const handleMessaging = async ( ) => { let { media, content, contentType, parentMessageId } = data; const { chatId, chatType, isReply, isForward } = data; + if ( (!isForward && !content && @@ -62,6 +63,8 @@ const handleMessaging = async ( } } + const isAppropriate = await detectInappropriateContent(content); + const message = new Message({ media, content, @@ -70,8 +73,12 @@ const handleMessaging = async ( senderId, chatId, parentMessageId, + isAppropriate, // Set the isAppropriate property based on the content check }); + + console.log(message); await message.save(); + if (parentMessage && isReply && chatType === 'channel') { parentMessage.threadMessages.push(message._id as Types.ObjectId); await parentMessage.save(); diff --git a/src/types/message.ts b/src/types/message.ts index ccfad88..57628a4 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -12,9 +12,10 @@ interface IMessage extends ICommunication { deliveredTo: Types.ObjectId[]; readBy: Types.ObjectId[]; senderId: Types.ObjectId; - chatId: Types.groupChannel; + chatId: Types.ObjectId; parentMessageId: Types.ObjectId | undefined; threadMessages: Types.ObjectId[]; + isAppropriate: boolean; } export default IMessage; From 89a94870bae2a77b88b0e568f5f9d73009653b8d Mon Sep 17 00:00:00 2001 From: AhmedHamed3699 Date: Fri, 20 Dec 2024 17:19:51 +0200 Subject: [PATCH 35/54] hotfix: solved a small typescript error --- src/controllers/authController.ts | 10 +- src/controllers/searchController.ts | 137 +++++++++++++++------------- src/services/authService.ts | 2 +- src/sockets/chats.ts | 1 - src/sockets/messages.ts | 6 +- src/types/message.ts | 2 - 6 files changed, 79 insertions(+), 79 deletions(-) diff --git a/src/controllers/authController.ts b/src/controllers/authController.ts index 0400952..ba3f2b3 100644 --- a/src/controllers/authController.ts +++ b/src/controllers/authController.ts @@ -79,12 +79,7 @@ export const login = catchAsync( ); if (user.accountStatus !== 'active') { - return next( - new AppError( - 'Your account is deactivated or banned.', - 403 - ) - ); + return next(new AppError('Your account is deactivated or banned.', 403)); } const message: string = await validateBeforeLogin(email, password); @@ -379,6 +374,3 @@ export const getCurrentSession = catchAsync( }); } ); - - - diff --git a/src/controllers/searchController.ts b/src/controllers/searchController.ts index e5b34ac..0a14b8b 100644 --- a/src/controllers/searchController.ts +++ b/src/controllers/searchController.ts @@ -1,30 +1,33 @@ -import { Response ,NextFunction } from 'express'; +import { Response, NextFunction } from 'express'; import Message from '@base/models/messageModel'; import Chat from '@base/models/chatModel'; import catchAsync from '@utils/catchAsync'; import GroupChannel from '@base/models/groupChannelModel'; import User from '@base/models/userModel'; import IUser from '@base/types/user'; -import IChat from '@base/types/chat'; import IGroupChannel from '@base/types/groupChannel'; -import IMessage from '@base/types/message'; -export const searchMessages = catchAsync(async (req: any, res: Response, next: NextFunction) => { - try { - let globalSearchResult: { + +export const searchMessages = catchAsync( + async (req: any, res: Response, next: NextFunction) => { + const globalSearchResult: { groups: IGroupChannel[]; users: IUser[]; channels: IGroupChannel[]; } = { groups: [], users: [], - channels: [] + channels: [], }; const { query, searchSpace, filter, isGlobalSearch } = req.body; // Input validation if (!query || !searchSpace || typeof isGlobalSearch === 'undefined') { - return res.status(400).json({ message: 'Query, searchSpace, and isGlobalSearch are required' }); + return res + .status(400) + .json({ + message: 'Query, searchSpace, and isGlobalSearch are required', + }); } const searchConditions: any = { content: { $regex: query, $options: 'i' } }; @@ -57,7 +60,9 @@ export const searchMessages = catchAsync(async (req: any, res: Response, next: N // Filter user chats by type const filteredChats = userChats.filter((chat) => - chatTypeConditions.length > 0 ? chatTypeConditions.some((cond) => cond.type === chat.type) : true + chatTypeConditions.length > 0 + ? chatTypeConditions.some((cond) => cond.type === chat.type) + : true ); const chatIds = filteredChats.map((chat) => chat._id); @@ -68,37 +73,34 @@ export const searchMessages = catchAsync(async (req: any, res: Response, next: N // Fetch messages and populate references const messages = await Message.find(finalSearchConditions) - .populate('senderId', 'username') - .populate({ - path: 'chatId', - select: 'name type', - }) - .limit(50); - - let groups: string[] = []; // Explicitly typing as string array - messages.forEach((message: IMessage) => { - groups.push(message.chatId.name); - console.log(message.chatId.name); - }); - - // Search for group channels by name in the groups array - const groupChannels = await GroupChannel.find({ - name: { $in: groups }, - }) - .select('name type picture'); - - // Now, populate the chatId with name, type, and picture - const updatedMessages = await Message.find(finalSearchConditions) - .populate({ - path: 'chatId', - select: 'name type picture', - match: { name: { $in: groups } }, // Ensure the chatId matches the groups array - }) - .limit(50); - - - - // This will print the 'name' from the chatId + .populate('senderId', 'username') + .populate({ + path: 'chatId', + select: 'name type', + }) + .limit(50); + + const groups: string[] = []; + messages.forEach((message: any) => { + groups.push(message.chatId.name); + console.log(message.chatId.name); + }); + + // Search for group channels by name in the groups array + const _groupChannels = await GroupChannel.find({ + name: { $in: groups }, + }).select('name type picture'); + + // Now, populate the chatId with name, type, and picture + const updatedMessages = await Message.find(finalSearchConditions) + .populate({ + path: 'chatId', + select: 'name type picture', + match: { name: { $in: groups } }, // Ensure the chatId matches the groups array + }) + .limit(50); + + // This will print the 'name' from the chatId // Global Search for Groups, Channels, and Chats if (isGlobalSearch) { // Groups and Channels by name @@ -106,18 +108,23 @@ export const searchMessages = catchAsync(async (req: any, res: Response, next: N name: { $regex: query, $options: 'i' }, }).select('name type picture'); - globalSearchResult.groups = groupsAndChannels.filter((gc: IGroupChannel) => gc.type === 'group'); - globalSearchResult.channels = groupsAndChannels.filter((gc: IGroupChannel) => gc.type === 'channel'); + globalSearchResult.groups = groupsAndChannels.filter( + (gc: IGroupChannel) => gc.type === 'group' + ); + globalSearchResult.channels = groupsAndChannels.filter( + (gc: IGroupChannel) => gc.type === 'channel' + ); // Users (to find chats involving usernames) const users = await User.find({ $or: [ { screenFirstName: { $regex: query, $options: 'i' } }, { screenLastName: { $regex: query, $options: 'i' } }, - { username: { $regex: query, $options: 'i' } } - ] - }).select('name username _id screenFirstName screenLastName phoneNumber photo bio accountStatus stories'); - + { username: { $regex: query, $options: 'i' } }, + ], + }).select( + 'name username _id screenFirstName screenLastName phoneNumber photo bio accountStatus stories' + ); globalSearchResult.users = users; @@ -135,25 +142,29 @@ export const searchMessages = catchAsync(async (req: any, res: Response, next: N success: true, data: { searchResult: updatedMessages, - globalSearchResult: globalSearchResult, + globalSearchResult, }, }); - } catch (error) { - console.error('Error in searchMessages:', error); - res.status(500).json({ success: false, message: 'Internal Server Error' }); } -}); - -export const searchMessagesDummmy = catchAsync(async (req: any, res: Response, next: NextFunction) => { - try { - const userId = req.user.id; - const { query, searchSpace, filter, isGlobalSearch } = req.query; - - if (!query || !searchSpace || typeof isGlobalSearch === 'undefined') { - return res.status(400).json({ message: 'Query, searchSpace, and isGlobalSearch are required' }); +); + +export const searchMessagesDummmy = catchAsync( + async (req: any, res: Response, next: NextFunction) => { + try { + const { query, searchSpace, isGlobalSearch } = req.query; + + if (!query || !searchSpace || typeof isGlobalSearch === 'undefined') { + return res + .status(400) + .json({ + message: 'Query, searchSpace, and isGlobalSearch are required', + }); + } + } catch (error) { + console.error('Error in searchMessages:', error); + res + .status(500) + .json({ success: false, message: 'Internal Server Error' }); } - } catch (error) { - console.error('Error in searchMessages:', error); - res.status(500).json({ success: false, message: 'Internal Server Error' }); } -}); +); diff --git a/src/services/authService.ts b/src/services/authService.ts index 5d813b0..1152491 100644 --- a/src/services/authService.ts +++ b/src/services/authService.ts @@ -180,4 +180,4 @@ export const createOAuthUser = async ( }); await newUser.save({ validateBeforeSave: false }); return newUser; -}; \ No newline at end of file +}; diff --git a/src/sockets/chats.ts b/src/sockets/chats.ts index ffc2967..987ef18 100644 --- a/src/sockets/chats.ts +++ b/src/sockets/chats.ts @@ -467,7 +467,6 @@ const handleSetPermission = async ( else if (type === 'download') chat.downloadingPermission = who === 'everyone'; await chat.save(); socket.to(chatId).emit('SET_PERMISSION_SERVER', { chatId, type, who }); - console.log(chat); ack({ success: true, message: 'permissions updated successfully', diff --git a/src/sockets/messages.ts b/src/sockets/messages.ts index be9147b..e35b592 100644 --- a/src/sockets/messages.ts +++ b/src/sockets/messages.ts @@ -3,6 +3,7 @@ import { Server, Socket } from 'socket.io'; import IMessage from '@base/types/message'; import Message from '@models/messageModel'; import { enableDestruction } from '@services/chatService'; +import { detectInappropriateContent } from '@services/googleAIService'; import Chat from '@base/models/chatModel'; import { check, informSessions, updateDraft } from './MessagingServices'; @@ -73,10 +74,9 @@ const handleMessaging = async ( senderId, chatId, parentMessageId, - isAppropriate, // Set the isAppropriate property based on the content check + isAppropriate, }); - - console.log(message); + await message.save(); if (parentMessage && isReply && chatType === 'channel') { diff --git a/src/types/message.ts b/src/types/message.ts index 57628a4..0e4b363 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -11,8 +11,6 @@ interface IMessage extends ICommunication { isAnnouncement: boolean; deliveredTo: Types.ObjectId[]; readBy: Types.ObjectId[]; - senderId: Types.ObjectId; - chatId: Types.ObjectId; parentMessageId: Types.ObjectId | undefined; threadMessages: Types.ObjectId[]; isAppropriate: boolean; From f0bba36e325f7ea71f860fc365224147d3ce2ac6 Mon Sep 17 00:00:00 2001 From: shehab299 Date: Fri, 20 Dec 2024 17:41:54 +0200 Subject: [PATCH 36/54] Add Github action to check code compilation --- .../checkCodeCompilationForPeter.yaml | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 .github/workflows/checkCodeCompilationForPeter.yaml diff --git a/.github/workflows/checkCodeCompilationForPeter.yaml b/.github/workflows/checkCodeCompilationForPeter.yaml new file mode 100644 index 0000000..d96f19b --- /dev/null +++ b/.github/workflows/checkCodeCompilationForPeter.yaml @@ -0,0 +1,34 @@ +name: Check That Code Compiles For Peter + +on: + push: + branches: + - develop + - main + + pull_request: + branches: + - main + - develop + +jobs: + check-code-compilation: + + runs-on: ubuntu-latest + + steps: + - name: Check out code + uses: actions/checkout@v2 + + - name: setup node + uses: actions/setup-node@v2 + with: + node-version: 'lts/*' + + - name: Install dependencies + run: npm install + + - name: Run Server + run: npm run build + + From f057450c2728e71266c7ca08e7a0230938e10e3b Mon Sep 17 00:00:00 2001 From: shehab299 Date: Fri, 20 Dec 2024 18:01:33 +0200 Subject: [PATCH 37/54] Updated checking code triggers --- .github/workflows/checkCodeCompilationForPeter.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/checkCodeCompilationForPeter.yaml b/.github/workflows/checkCodeCompilationForPeter.yaml index d96f19b..f4e4d85 100644 --- a/.github/workflows/checkCodeCompilationForPeter.yaml +++ b/.github/workflows/checkCodeCompilationForPeter.yaml @@ -11,6 +11,11 @@ on: - main - develop + pull_request_target: + branches: + - main + - develop + jobs: check-code-compilation: From 0ee9e5126f60538eba9f87ab1c2b0a172cfe926a Mon Sep 17 00:00:00 2001 From: AhmedHamed3699 Date: Fri, 20 Dec 2024 19:24:57 +0200 Subject: [PATCH 38/54] hotfix(messaging): AI integration bug --- src/database/seed/json/users.json | 24 ++++++++++++++++++++++-- src/models/messageModel.ts | 2 +- src/services/googleAIService.ts | 6 ++++-- src/sockets/MessagingServices.ts | 13 ++++++++++++- src/sockets/messages.ts | 14 +++++++------- 5 files changed, 46 insertions(+), 13 deletions(-) diff --git a/src/database/seed/json/users.json b/src/database/seed/json/users.json index 0ab2e2c..057b982 100644 --- a/src/database/seed/json/users.json +++ b/src/database/seed/json/users.json @@ -28,7 +28,7 @@ "phoneNumber": "+201055445511", "password": "12345678", "passwordConfirm": "12345678", - "accountStatus": "deactivated" + "accountStatus": "active" }, { "email": "front3@gmail.com", @@ -38,7 +38,7 @@ "phoneNumber": "+201055287611", "password": "12345678", "passwordConfirm": "12345678", - "accountStatus": "banned" + "accountStatus": "active" }, { "email": "front4@gmail.com", @@ -109,5 +109,25 @@ "password": "12345678", "passwordConfirm": "12345678", "accountStatus": "active" + }, + { + "email": "banned@gmail.com", + "username": "Forever_Banned", + "screenFirstName": "Lonely", + "screenLastName": "Soul", + "phoneNumber": "+201055900090", + "password": "12345678", + "passwordConfirm": "12345678", + "accountStatus": "banned" + }, + { + "email": "deactivated@gmail.com", + "username": "Forever_Deactivated", + "screenFirstName": "Stolen", + "screenLastName": "Soul", + "phoneNumber": "+201055900090", + "password": "12345678", + "passwordConfirm": "12345678", + "accountStatus": "deactivated" } ] diff --git a/src/models/messageModel.ts b/src/models/messageModel.ts index 5ad101a..61e12a0 100644 --- a/src/models/messageModel.ts +++ b/src/models/messageModel.ts @@ -16,7 +16,7 @@ const messageSchema = new mongoose.Schema({ }, isAppropriate: { type: Boolean, - default: false, + default: true, }, isForward: { type: Boolean, diff --git a/src/services/googleAIService.ts b/src/services/googleAIService.ts index 4f19eb3..de0e7c0 100644 --- a/src/services/googleAIService.ts +++ b/src/services/googleAIService.ts @@ -4,7 +4,7 @@ const hf = new HfInference(process.env.HF_API_KEY); const modelName = 'unitary/toxic-bert'; -export async function detectInappropriateContent(text: string): Promise { +async function detectInappropriateContent(text: string): Promise { try { const response = await hf.textClassification({ model: modelName, @@ -32,4 +32,6 @@ export async function detectInappropriateContent(text: string): Promise console.error('Error detecting inappropriate content:', error); throw new Error('Failed to detect inappropriate content'); } -} \ No newline at end of file +} + +export default detectInappropriateContent; \ No newline at end of file diff --git a/src/sockets/MessagingServices.ts b/src/sockets/MessagingServices.ts index fd4bc6d..a15fd30 100644 --- a/src/sockets/MessagingServices.ts +++ b/src/sockets/MessagingServices.ts @@ -6,6 +6,7 @@ import User from '@models/userModel'; import GroupChannel from '@models/groupChannelModel'; import Message from '@models/messageModel'; import IMessage from '@base/types/message'; +import detectInappropriateContent from '@base/services/googleAIService'; export interface Member { user: Types.ObjectId; @@ -18,7 +19,7 @@ export const check = async ( senderId: any, additionalData?: any ) => { - const { chatType, checkAdmin, newMessageIsReply } = additionalData; + const { chatType, checkAdmin, newMessageIsReply, content } = additionalData; if (!chat || chat.isDeleted) { return ack({ @@ -64,6 +65,16 @@ export const check = async ( success: false, message: 'only admins can post and reply to this chat', }); + if ( + chat?.type === 'group' && + !chat.isFilterd && + (await detectInappropriateContent(content)) + ) { + return ack({ + success: false, + message: 'inappropriate content', + }); + } if (chat.type === 'channel' && !newMessageIsReply) return ack({ success: false, diff --git a/src/sockets/messages.ts b/src/sockets/messages.ts index e35b592..52d782b 100644 --- a/src/sockets/messages.ts +++ b/src/sockets/messages.ts @@ -3,7 +3,6 @@ import { Server, Socket } from 'socket.io'; import IMessage from '@base/types/message'; import Message from '@models/messageModel'; import { enableDestruction } from '@services/chatService'; -import { detectInappropriateContent } from '@services/googleAIService'; import Chat from '@base/models/chatModel'; import { check, informSessions, updateDraft } from './MessagingServices'; @@ -45,6 +44,7 @@ const handleMessaging = async ( const chat = await Chat.findById(chatId); const func = await check(chat, ack, senderId, { newMessageIsReply: isReply, + content, }); if (!func) return; @@ -64,8 +64,6 @@ const handleMessaging = async ( } } - const isAppropriate = await detectInappropriateContent(content); - const message = new Message({ media, content, @@ -74,7 +72,6 @@ const handleMessaging = async ( senderId, chatId, parentMessageId, - isAppropriate, }); await message.save(); @@ -86,9 +83,12 @@ const handleMessaging = async ( await updateDraft(io, senderId, chatId, ''); socket.to(chatId).emit('RECEIVE_MESSAGE', message, async (res: any) => { - if (res.success) { - if (res.isRead) message.readBy.push(res.userId); - else message.deliveredTo.push(res.userId); + if (res.success && res.userId !== senderId) { + if (res.isRead && !message.readBy.includes(res.userId)) { + message.readBy.push(res.userId); + } else if (!message.deliveredTo.includes(res.userId)) { + message.deliveredTo.push(res.userId); + } message.save(); informSessions( io, From 79be90914fdb7b6866b32927c64f99e5a673f3cb Mon Sep 17 00:00:00 2001 From: AhmedHamed3699 Date: Fri, 20 Dec 2024 19:38:49 +0200 Subject: [PATCH 39/54] hotfix(seed): solved username bug --- src/database/seed/json/users.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/database/seed/json/users.json b/src/database/seed/json/users.json index 057b982..a01d07a 100644 --- a/src/database/seed/json/users.json +++ b/src/database/seed/json/users.json @@ -122,7 +122,7 @@ }, { "email": "deactivated@gmail.com", - "username": "Forever_Deactivated", + "username": "I_HOPE_I_RETURN", "screenFirstName": "Stolen", "screenLastName": "Soul", "phoneNumber": "+201055900090", From a428ba67662061a0b4a5ff758091e173887b587d Mon Sep 17 00:00:00 2001 From: Peter Safwat <118059454+PeterSafwatHBK@users.noreply.github.com> Date: Fri, 20 Dec 2024 22:06:16 +0200 Subject: [PATCH 40/54] feat(chats): apply unfiltering --- src/controllers/chatController.ts | 29 +++++++++++++++++++++++------ src/routes/chatRoute.ts | 8 +++++--- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/controllers/chatController.ts b/src/controllers/chatController.ts index 16da07f..c1c0123 100644 --- a/src/controllers/chatController.ts +++ b/src/controllers/chatController.ts @@ -333,13 +333,29 @@ export const getVoiceCallsInChat = catchAsync( ); export const filterChatGroups = catchAsync( async (req: Request, res: Response, next: NextFunction) => { - const { chatId } = req.params; + const chatId = req.params.chatId; + console.log(chatId); + const groupChannel = await GroupChannel.findById(chatId); + if (!groupChannel) { + return res.status(404).json({ + status: 'fail', + message: 'Group/Channel not found with the given chatId', + }); + } + + groupChannel.isFilterd = true; + res.status(200).json({ + status: 'success', + message: 'isFiltered set to true successfully', + data: groupChannel, + }); + } +); +export const unfilterChatGroups = catchAsync( + async (req: Request, res: Response, next: NextFunction) => { + const chatId = req.params.chatId; - const groupChannel = await GroupChannel.findOneAndUpdate( - { chatId }, - { isFiltered: true }, - { new: true } - ); + const groupChannel = await GroupChannel.findById(chatId); if (!groupChannel) { return res.status(404).json({ @@ -347,6 +363,7 @@ export const filterChatGroups = catchAsync( message: 'GroupChannel not found with the given chatId', }); } + groupChannel.isFilterd = false; res.status(200).json({ status: 'success', diff --git a/src/routes/chatRoute.ts b/src/routes/chatRoute.ts index af928ba..5a92258 100644 --- a/src/routes/chatRoute.ts +++ b/src/routes/chatRoute.ts @@ -14,9 +14,10 @@ import { invite, join, getVoiceCallsInChat, - filterChatGroups + filterChatGroups, + unfilterChatGroups } from '@base/controllers/chatController'; -import { protect } from '@base/middlewares/authMiddleware'; +import { protect , isAdmin } from '@base/middlewares/authMiddleware'; import upload from '@base/config/fileUploads'; import restrictTo from '@base/middlewares/chatMiddlewares'; @@ -46,6 +47,7 @@ router.get('/messages/:chatId', restrictTo(), getMessages); router.get('/members/:chatId', restrictTo(), getChatMembers); router.get('/:chatId', restrictTo(), getChat); -router.patch('/groups/filter', restrictTo('admin'), filterChatGroups); +router.patch('/groups/filter/:chatId', isAdmin, filterChatGroups); +router.patch('/groups/unfilter/:chatId', isAdmin, unfilterChatGroups); export default router; From cccef7e5f8a7d755130f3287ada702268c2e0c79 Mon Sep 17 00:00:00 2001 From: Peter Safwat <118059454+PeterSafwatHBK@users.noreply.github.com> Date: Fri, 20 Dec 2024 23:20:17 +0200 Subject: [PATCH 41/54] Update chatController.ts --- src/controllers/chatController.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/controllers/chatController.ts b/src/controllers/chatController.ts index c1c0123..369f388 100644 --- a/src/controllers/chatController.ts +++ b/src/controllers/chatController.ts @@ -344,6 +344,7 @@ export const filterChatGroups = catchAsync( } groupChannel.isFilterd = true; + await groupChannel.save(); res.status(200).json({ status: 'success', message: 'isFiltered set to true successfully', @@ -364,6 +365,7 @@ export const unfilterChatGroups = catchAsync( }); } groupChannel.isFilterd = false; + await groupChannel.save(); res.status(200).json({ status: 'success', From f012bf89f49d027532def9ba0b5deffa6b21c9b9 Mon Sep 17 00:00:00 2001 From: Peter Safwat <118059454+PeterSafwatHBK@users.noreply.github.com> Date: Fri, 20 Dec 2024 23:58:42 +0200 Subject: [PATCH 42/54] Update userController.ts (#165) --- src/controllers/userController.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts index fca94a6..75b1e61 100644 --- a/src/controllers/userController.ts +++ b/src/controllers/userController.ts @@ -107,7 +107,7 @@ export const getUser = catchAsync(async (req: GetUser, res: Response) => { export const getAllUsers = catchAsync(async (req: Request, res: Response) => { const users = await User.find( {}, - 'username screenFirstName screenLastName email photo status bio' + 'username screenFirstName screenLastName email photo status bio accountStatus' ); return res.status(200).json({ @@ -335,4 +335,4 @@ export const banUser = catchAsync(async (req: Request, res: Response) => { status: 'success', message: 'User banned successfully', }); -}); \ No newline at end of file +}); From 3c04a5c577f8608b71c33f927d0c5c66b875f7d5 Mon Sep 17 00:00:00 2001 From: akramhany Date: Sat, 21 Dec 2024 03:42:35 +0200 Subject: [PATCH 43/54] chore(logs): log something in voice calls --- src/sockets/voiceCalls.ts | 4 +++- src/sockets/voiceCallsServices.ts | 3 +++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/src/sockets/voiceCalls.ts b/src/sockets/voiceCalls.ts index f4598ad..e1d10e3 100644 --- a/src/sockets/voiceCalls.ts +++ b/src/sockets/voiceCalls.ts @@ -36,6 +36,7 @@ async function handleCreateCall( chatId = '123'; } + console.log('User Started Call: ', userId); const voiceCall = await createVoiceCall(chatId, userId); io.to(chatId).emit('CALL-STARTED', { @@ -52,7 +53,8 @@ async function handleJoinCall( userId: string ) { const { voiceCallId } = data; - + console.log('Client joined a call, CallId:', voiceCallId); + console.log('UserId: ', userId); await addClientToCall(socket, userId, voiceCallId); socket.join(voiceCallId); diff --git a/src/sockets/voiceCallsServices.ts b/src/sockets/voiceCallsServices.ts index c031da8..40f8972 100644 --- a/src/sockets/voiceCallsServices.ts +++ b/src/sockets/voiceCallsServices.ts @@ -39,6 +39,8 @@ export async function createVoiceCall(chatId: string, userId: string) { await voiceCall.save(); + console.log('Voice Call created: ', voiceCall._id); + return voiceCall; } @@ -71,6 +73,7 @@ export async function addClientToCall( // Add the client socket id into the map if (!clientSocketMap[voiceCallId]) clientSocketMap[voiceCallId] = {}; clientSocketMap[voiceCallId][userId] = socket.id; + console.log('clientSocketMap: ', clientSocketMap[voiceCallId]); } export async function removeClientFromCall( From 3732fced1e42c8048b3c878a55572892621b6631 Mon Sep 17 00:00:00 2001 From: AhmedHamed3699 Date: Sat, 21 Dec 2024 05:43:16 +0200 Subject: [PATCH 44/54] fix: solved various bugs --- src/controllers/chatController.ts | 94 +---------- src/models/chatModel.ts | 9 ++ src/routes/chatRoute.ts | 12 +- src/services/chatService.ts | 34 ++-- src/sockets/MessagingServices.ts | 5 +- src/sockets/chats.ts | 260 +++++++++++++++++++----------- src/sockets/messages.ts | 1 + 7 files changed, 209 insertions(+), 206 deletions(-) diff --git a/src/controllers/chatController.ts b/src/controllers/chatController.ts index 369f388..2fbbbdb 100644 --- a/src/controllers/chatController.ts +++ b/src/controllers/chatController.ts @@ -1,12 +1,10 @@ import AppError from '@base/errors/AppError'; import Chat from '@base/models/chatModel'; import Message from '@base/models/messageModel'; -import NormalChat from '@base/models/normalChatModel'; import User from '@base/models/userModel'; import { getChats, getLastMessage, - unmute, deleteChatPictureFile, getUnreadMessages, } from '@base/services/chatService'; @@ -65,12 +63,13 @@ export const getMessages = catchAsync( const pageByMsgId = req.query.page === '0' ? undefined : req.query.page; const limit: number = parseInt(req.query.limit as string, 10) || 20; const filter: any = { chatId }; - if (pageByMsgId) { - filter._id = { $lt: pageByMsgId }; - } if (req.query.timestamp) { filter.timestamp = { $gte: req.query.timestamp }; } + else if (pageByMsgId) { + const message = await Message.findById(pageByMsgId); + filter.timestamp = { $lt: message.timestamp }; + } const messages = await Message.find(filter) .sort({ timestamp: -1 }) @@ -105,41 +104,6 @@ export const postMediaFile = catchAsync(async (req: any, res: Response) => { }); }); -export const enableSelfDestructing = catchAsync( - async (req: Request, res: Response, next: NextFunction) => { - const { chatId } = req.params; - const { destructionDuration } = req.body; - const destructionTimestamp = Date.now(); - if (!destructionDuration) - return next(new AppError('missing required fields', 400)); - const chat = await NormalChat.findByIdAndUpdate(chatId, { - destructionDuration, - destructionTimestamp, - }); - - if (!chat) return next(new AppError('No chat with the provided id', 404)); - res.status(200).json({ - status: 'success', - message: 'Destruction time is enabled successfuly', - }); - } -); - -export const disableSelfDestructing = catchAsync( - async (req: Request, res: Response, next: NextFunction) => { - const { chatId } = req.params; - const chat = NormalChat.findByIdAndUpdate(chatId, { - destructionTimestamp: undefined, - destructionDuration: undefined, - }); - if (!chat) return next(new AppError('No chat with the provided id', 404)); - res.status(200).json({ - status: 'success', - message: 'Destruction time is disabled successfuly', - }); - } -); - export const getChat = catchAsync(async (req: Request, res: Response) => { const { chatId } = req.params; const chat = await Chat.findById(chatId).populate( @@ -194,49 +158,6 @@ export const getChatMembers = catchAsync( } ); -export const muteChat = catchAsync( - async (req: Request, res: Response, next: NextFunction) => { - const { chatId } = req.params; - const { muteDuration } = req.body; - const user: IUser = req.user as IUser; - if (!user) return next(new AppError('login first', 403)); - if (!muteDuration) - return next(new AppError('missing required fields', 400)); - user.chats.forEach((c: any) => { - if (c.chat.equals(chatId)) { - c.isMuted = true; - c.muteDuration = muteDuration; - } - }); - await user.save({ validateBeforeSave: false }); - unmute(user, chatId, muteDuration); - res.status(200).json({ - status: 'success', - message: 'Chat muted successfully', - }); - } -); - -export const unmuteChat = catchAsync( - async (req: Request, res: Response, next: NextFunction) => { - const { chatId } = req.params; - const user: IUser = req.user as IUser; - if (!user) return next(new AppError('login first', 403)); - - user.chats.forEach((c: any) => { - if (c.chat.equals(chatId)) { - c.isMuted = false; - c.muteDuration = undefined; - } - }); - await user.save({ validateBeforeSave: false }); - res.status(200).json({ - status: 'success', - message: 'Chat unmuted successfully', - }); - } -); - export const updateChatPicture = catchAsync(async (req: any, res: Response) => { const { chatId } = req.params; @@ -333,8 +254,7 @@ export const getVoiceCallsInChat = catchAsync( ); export const filterChatGroups = catchAsync( async (req: Request, res: Response, next: NextFunction) => { - const chatId = req.params.chatId; - console.log(chatId); + const { chatId } = req.params; const groupChannel = await GroupChannel.findById(chatId); if (!groupChannel) { return res.status(404).json({ @@ -342,7 +262,7 @@ export const filterChatGroups = catchAsync( message: 'Group/Channel not found with the given chatId', }); } - + groupChannel.isFilterd = true; await groupChannel.save(); res.status(200).json({ @@ -354,7 +274,7 @@ export const filterChatGroups = catchAsync( ); export const unfilterChatGroups = catchAsync( async (req: Request, res: Response, next: NextFunction) => { - const chatId = req.params.chatId; + const { chatId } = req.params; const groupChannel = await GroupChannel.findById(chatId); diff --git a/src/models/chatModel.ts b/src/models/chatModel.ts index 5adc6b0..33684e1 100644 --- a/src/models/chatModel.ts +++ b/src/models/chatModel.ts @@ -62,5 +62,14 @@ chatSchema.virtual('numberOfMembers').get(function () { return Array.isArray(this.members) ? this.members.length : 0; }); +chatSchema.pre('save', function (next) { + if (!this.isModified('members')) return next(); + const uniqueUsers = new Set(this.members.map((m) => m.user.toString())); + if (uniqueUsers.size !== this.members.length) { + return next(new Error('Members must have unique users.')); + } + next(); +}); + const Chat = mongoose.model('Chat', chatSchema); export default Chat; diff --git a/src/routes/chatRoute.ts b/src/routes/chatRoute.ts index 5a92258..33ef9df 100644 --- a/src/routes/chatRoute.ts +++ b/src/routes/chatRoute.ts @@ -3,21 +3,17 @@ import { getAllChats, getMessages, postMediaFile, - enableSelfDestructing, - disableSelfDestructing, getChat, setPrivacy, getChatMembers, - muteChat, - unmuteChat, updateChatPicture, invite, join, getVoiceCallsInChat, filterChatGroups, - unfilterChatGroups + unfilterChatGroups, } from '@base/controllers/chatController'; -import { protect , isAdmin } from '@base/middlewares/authMiddleware'; +import { protect, isAdmin } from '@base/middlewares/authMiddleware'; import upload from '@base/config/fileUploads'; import restrictTo from '@base/middlewares/chatMiddlewares'; @@ -34,10 +30,6 @@ router.patch( ); router.patch('/privacy/:chatId', restrictTo('admin'), setPrivacy); -router.patch('/destruct/:chatId', restrictTo(), enableSelfDestructing); -router.patch('/un-destruct/:chatId', restrictTo(), disableSelfDestructing); -router.patch('/mute/:chatId', restrictTo(), muteChat); -router.patch('/unmute/:chatId', restrictTo(), unmuteChat); router.get('/invite/:chatId', restrictTo('admin'), invite); router.post('/join/:token', join); diff --git a/src/services/chatService.ts b/src/services/chatService.ts index 2e782c2..cc92021 100644 --- a/src/services/chatService.ts +++ b/src/services/chatService.ts @@ -1,12 +1,12 @@ import mongoose from 'mongoose'; import NormalChat from '@base/models/normalChatModel'; import Message from '@base/models/messageModel'; -import { Socket } from 'socket.io'; +import { Server, Socket } from 'socket.io'; import User from '@base/models/userModel'; -import IUser from '@base/types/user'; import AppError from '@base/errors/AppError'; import GroupChannel from '@base/models/groupChannelModel'; import deleteFile from '@base/utils/deleteFile'; +import { informSessions } from '@base/sockets/MessagingServices'; export const getLastMessage = async (chats: any) => { const lastMessages = await Promise.all( @@ -76,20 +76,26 @@ export const enableDestruction = async ( } }; -export const unmute = async ( - user: IUser, +export const muteUnmuteChat = async ( + io: Server, + userId: string, chatId: string, - muteDuration: number + event: string, + muteDuration?: number ) => { - setTimeout(async () => { - user.chats.forEach((c: any) => { - if (c.chat.equals(chatId)) { - c.isMuted = false; - c.muteDuration = undefined; - } - }); - await user.save({ validateBeforeSave: false }); - }, muteDuration * 1000); + User.findByIdAndUpdate( + userId, + { + $set: { + 'chats.$[elem].isMuted': muteDuration, + 'chats.$[elem].muteDuration': muteDuration, + }, + }, + { + arrayFilters: [{ 'elem.chat': chatId }], + } + ); + informSessions(io, userId, { chatId }, event); }; export const deleteChatPictureFile = async ( diff --git a/src/sockets/MessagingServices.ts b/src/sockets/MessagingServices.ts index a15fd30..ca076c8 100644 --- a/src/sockets/MessagingServices.ts +++ b/src/sockets/MessagingServices.ts @@ -19,7 +19,8 @@ export const check = async ( senderId: any, additionalData?: any ) => { - const { chatType, checkAdmin, newMessageIsReply, content } = additionalData; + const { chatType, checkAdmin, newMessageIsReply, content, sendMessage } = + additionalData; if (!chat || chat.isDeleted) { return ack({ @@ -57,7 +58,7 @@ export const check = async ( message: 'you do not have permission as you are not an admin', }); - if (sender.Role !== 'admin' && chat.type !== 'private') { + if (sendMessage && sender.Role !== 'admin' && chat.type !== 'private') { const groupChannelChat = await GroupChannel.findById(chat._id); if (!groupChannelChat.messagingPermission) diff --git a/src/sockets/chats.ts b/src/sockets/chats.ts index 987ef18..d8f661f 100644 --- a/src/sockets/chats.ts +++ b/src/sockets/chats.ts @@ -4,11 +4,12 @@ import User from '@models/userModel'; import Chat from '@models/chatModel'; import GroupChannel from '@models/groupChannelModel'; import NormalChat from '@base/models/normalChatModel'; +import { muteUnmuteChat } from '@base/services/chatService'; +import IUser from '@base/types/user'; import { check, informSessions, joinRoom, - Member, updateDraft, } from './MessagingServices'; @@ -45,7 +46,7 @@ const handleAddAdmins = async ( return; } - Chat.findByIdAndUpdate( + await Chat.findByIdAndUpdate( chatId, { $set: { 'members.$[elem].Role': 'admin' } }, { @@ -311,27 +312,20 @@ const handleLeaveGroupChannel = async ( senderId: any ) => { const { chatId } = data; - const chat = await Chat.findById(chatId); - if (!chat || chat.isDeleted) - return ack({ - success: false, - message: 'could not leave the group', - error: 'this chat does no longer exist', - }); - const isMember = chat.members.some( - (member: any) => member.user.toString() === senderId.toString() - ); - if (!isMember) - return ack({ - success: false, - message: 'could not leave the group', - error: 'you are not a member of this chat', - }); + const chat = await GroupChannel.findById(chatId); + const func = await check(chat, ack, senderId, { + chatType: ['group', 'channel'], + }); + if (!func) return func; - await Chat.updateOne( - { _id: chatId }, - { $pull: { members: { user: senderId } } } - ); + await Promise.all([ + GroupChannel.findByIdAndUpdate(chatId, { + $pull: { members: { user: senderId } }, + }), + User.findByIdAndUpdate(senderId, { + $pull: { chats: { chat: chatId } }, + }), + ]); socket .to(chatId) @@ -351,65 +345,51 @@ const handleRemoveMembers = async ( senderId: any ) => { const { chatId, members } = data; - const forbiddenUsers: string[] = []; - - const chat = await Chat.findById(chatId); - if (!chat || chat.isDeleted) - return ack({ - success: false, - message: 'could not remove members from the group', - error: 'this chat does no longer exist', - }); - - const admin: Member = chat.members.find((m) => - m.user.equals(senderId) - ) as unknown as Member; + const invalidUsers: string[] = []; - if (!admin) - return ack({ - success: false, - message: 'could not remove members from the group', - error: 'you are no longer a member of this group', - }); + const chat = await GroupChannel.findById(chatId); - if (admin.Role === 'member') - return ack({ - success: false, - message: 'could not remove members from the group', - error: 'you do not have permission', - }); + const func = await check(chat, ack, senderId, { + chatType: ['group', 'channel'], + checkAdmin: true, + }); + if (!func) return func; await Promise.all( members.map(async (memberId: any) => { const user = await User.findById(memberId); if (!user) { - forbiddenUsers.push(memberId); + invalidUsers.push(memberId); return; } const isMember = chat.members.some((m: any) => m.user.equals(memberId)); if (!isMember) { - forbiddenUsers.push(memberId); + invalidUsers.push(memberId); return; } - await Chat.updateOne( - { _id: chatId }, - { $pull: { members: { user: memberId } } } - ); - - await informSessions(io, memberId, { chatId }, 'REMOVE_MEMBERS_SERVER'); + await Promise.all([ + GroupChannel.findByIdAndUpdate(chatId, { + $pull: { members: { user: memberId } }, + }), + User.findByIdAndUpdate(memberId, { + $pull: { chats: { chat: chatId } }, + }), + ]); }) ); - if (forbiddenUsers.length > 0) - return ack({ - success: false, - message: 'Some users could not be added', - error: `Could not remove users with IDs: ${forbiddenUsers.join(', ')}`, - }); + + socket + .to(chatId) + .emit('REMOVE_MEMBERS_SERVER', { chatId, memberId: senderId }); + ack({ success: true, - message: 'Members removed successfully', + message: + invalidUsers.length > 0 + ? `Some users could not be removed, IDs: ${invalidUsers.join(', ')}` + : 'Members removed successfully', data: {}, }); }; @@ -431,37 +411,11 @@ const handleSetPermission = async ( }); const chat = await GroupChannel.findById(chatId); - if (!chat || chat.isDeleted) - return ack({ - success: false, - message: 'could not update permissions', - error: 'this chat does no longer exist', - }); - - if (chat.type === 'private') - return ack({ - success: false, - message: 'could not update permissions', - error: 'cannot change permissions for private chats', - }); - - const admin: Member = chat.members.find((m: Member) => - m.user.equals(senderId) - ) as unknown as Member; - - if (!admin) - return ack({ - success: false, - message: 'could not update permissions', - error: 'you are no longer a member of this group', - }); - - if (admin.Role === 'member') - return ack({ - success: false, - message: 'could not change group permissions', - error: 'you do not have permission', - }); + const func = await check(chat, ack, senderId, { + chatType: ['group', 'channel'], + checkAdmin: true, + }); + if (!func) return func; if (type === 'post') chat.messagingPermission = who === 'everyone'; else if (type === 'download') chat.downloadingPermission = who === 'everyone'; @@ -474,6 +428,106 @@ const handleSetPermission = async ( }); }; +const handleSetPrivacy = async ( + io: any, + socket: Socket, + data: any, + ack: Function, + senderId: any +) => { + const { chatId, privacy } = data; + + const chat = await GroupChannel.findById(chatId); + const func = await check(chat, ack, senderId, { + chatType: ['group', 'channel'], + checkAdmin: true, + }); + if (!func) return func; + + chat.privacy = privacy; + await chat.save(); + socket.to(chatId).emit('SET_PRIVACY_SERVER', { chatId, privacy }); + ack({ + success: true, + message: 'privacy updated successfully', + data: {}, + }); +}; + +const handleMuteChat = async ( + io: any, + socket: Socket, + data: any, + ack: Function, + senderId: any +) => { + const { muteDuration, chatId } = data; + const userId = ((await User.findById(senderId)) as IUser) + ._id as Types.ObjectId; + + await muteUnmuteChat( + io, + userId.toString(), + chatId, + 'MUTE_CHAT', + muteDuration + ); + if (muteDuration !== -1) { + setTimeout(async () => { + muteUnmuteChat(io, userId.toString(), chatId, 'UNMUTE_CHAT'); + }, muteDuration * 1000); + } + + ack({ success: true, message: 'Chat muted successfully' }); +}; + +const handleUnMuteChat = async ( + io: any, + socket: Socket, + data: any, + ack: Function, + senderId: any +) => { + const { chatId } = data; + const userId = ((await User.findById(senderId)) as IUser) + ._id as Types.ObjectId; + await muteUnmuteChat(io, userId.toString(), chatId, 'UNMUTE_CHAT'); + ack({ success: true, message: 'Chat unmuted successfully' }); +}; + +const handleEnableDestruction = async ( + io: any, + socket: Socket, + data: any, + ack: Function, + senderId: any +) => { + const { destructionDuration, chatId } = data; + const destructionTimestamp = Date.now(); + await NormalChat.findByIdAndUpdate(chatId, { + destructionDuration, + destructionTimestamp, + }); + await informSessions(io, senderId, { chatId }, 'ENABLE_DESTRUCTION_SERVER'); + ack({ success: true, message: 'Chat enabled destruction successfully' }); +}; + +const handleDisableDestruction = async ( + io: any, + socket: Socket, + data: any, + ack: Function, + senderId: any +) => { + const { chatId } = data; + await NormalChat.findByIdAndUpdate(chatId, { + destructionTimestamp: undefined, + destructionDuration: undefined, + }); + await informSessions(io, senderId, { chatId }, 'ENABLE_DESTRUCTION_SERVER'); + ack({ success: true, message: 'Chat disabled destruction successfully' }); +}; + const handleDraftMessage = async ( io: Server, socket: Socket, @@ -507,6 +561,10 @@ const registerChatHandlers = (io: Server, socket: Socket, userId: any) => { handleSetPermission(io, socket, data, ack, userId); }); + socket.on('SET_PRIVACY_CLIENT', (data: any, ack: Function) => { + handleSetPrivacy(io, socket, data, ack, userId); + }); + socket.on('ADD_ADMINS_CLIENT', (data: any, ack: Function) => { handleAddAdmins(io, socket, data, ack, userId); }); @@ -519,6 +577,22 @@ const registerChatHandlers = (io: Server, socket: Socket, userId: any) => { handleRemoveMembers(io, socket, data, ack, userId); }); + socket.on('MUTE_CHAT_CLIENT', (data: any, ack: Function) => { + handleMuteChat(io, socket, data, ack, userId); + }); + + socket.on('UNMUTE_CHAT_CLIENT', (data: any, ack: Function) => { + handleUnMuteChat(io, socket, data, ack, userId); + }); + + socket.on('ENABLE_DESTRUCTION_CLIENT', (data: any, ack: Function) => { + handleEnableDestruction(io, socket, data, ack, userId); + }); + + socket.on('DISABLE_DESTRUCTION_CLIENT', (data: any, ack: Function) => { + handleDisableDestruction(io, socket, data, ack, userId); + }); + socket.on('UPDATE_DRAFT_CLIENT', (data: any, ack: Function) => handleDraftMessage(io, socket, data, ack, userId) ); diff --git a/src/sockets/messages.ts b/src/sockets/messages.ts index 52d782b..01c050d 100644 --- a/src/sockets/messages.ts +++ b/src/sockets/messages.ts @@ -45,6 +45,7 @@ const handleMessaging = async ( const func = await check(chat, ack, senderId, { newMessageIsReply: isReply, content, + sendMessage: true, }); if (!func) return; From baceab22498b8fc7e3832d9f49819d2055802634 Mon Sep 17 00:00:00 2001 From: Peter Safwat <118059454+PeterSafwatHBK@users.noreply.github.com> Date: Sat, 21 Dec 2024 08:39:04 +0200 Subject: [PATCH 45/54] Update userRoute.ts (#168) --- src/routes/userRoute.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/routes/userRoute.ts b/src/routes/userRoute.ts index 1e0745f..8efc856 100644 --- a/src/routes/userRoute.ts +++ b/src/routes/userRoute.ts @@ -67,7 +67,7 @@ router.delete('/picture', deletePicture); router.get('/contacts/stories', getAllContactsStories); router.get('/:userId/stories', getStory); router.get('/:userId', getUser); -router.get('/',isAdmin, getAllUsers); +router.get('/', getAllUsers); export default router; From 20c28739a627c9ec81c7f90707d605638797ffe6 Mon Sep 17 00:00:00 2001 From: Akram Hany <109467185+akramhany@users.noreply.github.com> Date: Sat, 21 Dec 2024 14:24:29 +0200 Subject: [PATCH 46/54] chore(logs):put some logs (#170) --- src/sockets/voiceCalls.ts | 11 +++++++++-- src/sockets/voiceCallsServices.ts | 2 ++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/sockets/voiceCalls.ts b/src/sockets/voiceCalls.ts index e1d10e3..22db3c8 100644 --- a/src/sockets/voiceCalls.ts +++ b/src/sockets/voiceCalls.ts @@ -53,8 +53,9 @@ async function handleJoinCall( userId: string ) { const { voiceCallId } = data; - console.log('Client joined a call, CallId:', voiceCallId); - console.log('UserId: ', userId); + console.log( + `Client Joined call, clientId: ${userId} , callId: ${voiceCallId}` + ); await addClientToCall(socket, userId, voiceCallId); socket.join(voiceCallId); @@ -73,6 +74,10 @@ async function handleSignal( ) { const { type, targetId, voiceCallId, data } = signalData; + console.log( + `Signal Sent, type: ${type}, senderId: ${userId}, targetId: ${targetId}, voiceCallId: ${voiceCallId}` + ); + const socketId = getClientSocketId(voiceCallId, targetId); io.to(socketId).emit('SIGNAL-CLIENT', { @@ -91,6 +96,8 @@ async function handleLeaveCall( ) { const { voiceCallId } = data; + console.log(`Client Left, clientId: ${userId}, voiceCallId: ${voiceCallId}`); + socket.leave(voiceCallId); await removeClientFromCall(userId, voiceCallId); diff --git a/src/sockets/voiceCallsServices.ts b/src/sockets/voiceCallsServices.ts index 40f8972..7570d57 100644 --- a/src/sockets/voiceCallsServices.ts +++ b/src/sockets/voiceCallsServices.ts @@ -97,6 +97,8 @@ export async function removeClientFromCall( if (voiceCall.currentParticipants.length === 0) { await endVoiceCall(voiceCallId); } + + console.log('clientSocketMap: ', clientSocketMap[voiceCallId]); } export function getClientSocketMap(): ClientSocketMap { From 0cfbc94f3bba9e39a7f5ef8b1219f0d131146c74 Mon Sep 17 00:00:00 2001 From: somiaelshemy Date: Sat, 21 Dec 2024 15:58:08 +0200 Subject: [PATCH 47/54] fix(message): fix announcements --- src/sockets/messages.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sockets/messages.ts b/src/sockets/messages.ts index 01c050d..9431798 100644 --- a/src/sockets/messages.ts +++ b/src/sockets/messages.ts @@ -19,7 +19,7 @@ const handleMessaging = async ( senderId: string ) => { let { media, content, contentType, parentMessageId } = data; - const { chatId, chatType, isReply, isForward } = data; + const { chatId, chatType, isReply, isForward, isAnnouncement } = data; if ( (!isForward && @@ -73,6 +73,7 @@ const handleMessaging = async ( senderId, chatId, parentMessageId, + isAnnouncement, }); await message.save(); From 07926a61b5909a6d1f8e6fa8038c8dc58696dfd2 Mon Sep 17 00:00:00 2001 From: AhmedHamed3699 Date: Sat, 21 Dec 2024 19:34:01 +0200 Subject: [PATCH 48/54] feat(messages): added mentions --- docs/api/chat.swagger.ts | 20 ++++++++++++----- docs/api/sockets.swagger.ts | 3 +++ src/controllers/chatController.ts | 2 +- src/services/chatService.ts | 14 +++++++----- src/sockets/chats.ts | 37 +++++++++---------------------- 5 files changed, 38 insertions(+), 38 deletions(-) diff --git a/docs/api/chat.swagger.ts b/docs/api/chat.swagger.ts index d397fd8..177256e 100644 --- a/docs/api/chat.swagger.ts +++ b/docs/api/chat.swagger.ts @@ -124,6 +124,20 @@ * items: * type: string * description: IDs of users blocked by this member. + * unreadMessages: + * type: array + * items: + * type: object + * properties: + * chatId: + * type: string + * description: Unique identifier of the chat this message belongs to. + * unreadMessagesCount: + * type: string + * description: Number of unread messages in the chat. + * isMentioned: + * type: boolean + * description: Indicates if the user is mentioned in the chat. * lastMessages: * type: array * items: @@ -171,12 +185,6 @@ * type: string * format: date-time * description: Timestamp when the message was sent. - * __v: - * type: integer - * description: Version key. - * id: - * type: string - * description: Alias for `_id`. * 401: * description: User is not logged in or the request is invalid. * content: diff --git a/docs/api/sockets.swagger.ts b/docs/api/sockets.swagger.ts index 785f66b..b031547 100644 --- a/docs/api/sockets.swagger.ts +++ b/docs/api/sockets.swagger.ts @@ -45,6 +45,9 @@ * isForward: * type: boolean * description: Indicates if the message is forwarded. + * isAnouncement: + * type: boolean + * description: Indicates if the message is an anouncement. * responses: * 200: * description: Message sent successfully. diff --git a/src/controllers/chatController.ts b/src/controllers/chatController.ts index 2fbbbdb..61fd401 100644 --- a/src/controllers/chatController.ts +++ b/src/controllers/chatController.ts @@ -45,7 +45,7 @@ export const getAllChats = catchAsync( 'username screenFirstName screenLastName phoneNumber photo status isAdmin stories blockedUsers' ), getLastMessage(allChats), - getUnreadMessages(allChats, userId), + getUnreadMessages(allChats, user), ]); res.status(200).json({ diff --git a/src/services/chatService.ts b/src/services/chatService.ts index cc92021..f3405f7 100644 --- a/src/services/chatService.ts +++ b/src/services/chatService.ts @@ -25,17 +25,21 @@ export const getLastMessage = async (chats: any) => { return lastMessages; }; -export const getUnreadMessages = async ( - chats: any, - userId: mongoose.Types.ObjectId -) => +export const getUnreadMessages = async (chats: any, user: any) => Promise.all( chats.map(async (chat: any) => ({ chatId: chat.chat._id, unreadMessagesCount: await Message.countDocuments({ chatId: chat.chat._id, - readBy: { $nin: [userId] }, + readBy: { $nin: [user._id] }, }), + isMentioned: + (await Message.exists({ + chatId: chat.chat._id, + readBy: { $nin: [user._id] }, + senderId: { $ne: user._id }, + content: new RegExp(`@${user.username}`, 'i'), + })) !== null, })) ); diff --git a/src/sockets/chats.ts b/src/sockets/chats.ts index d8f661f..1faad14 100644 --- a/src/sockets/chats.ts +++ b/src/sockets/chats.ts @@ -121,7 +121,7 @@ const handleAddMembers = async ( chat?.members.push({ user: userId, Role: 'member' }); const userWasMember = user.chats.some((c: any) => c.chat.equals(chatId)); if (!userWasMember) - User.findByIdAndUpdate( + await User.findByIdAndUpdate( userId, { $push: { chats: { chat: chatId } } }, { new: true } @@ -263,35 +263,20 @@ const handleDeleteGroupChannel = async ( senderId: any ) => { const { chatId } = data; - const chat = await Chat.findById(chatId); - if (!chat || chat.isDeleted) - return ack({ - success: false, - message: 'Could not delete the group', - error: 'no chat found with the provided id', - }); + const chat = await GroupChannel.findById(chatId); + const func = await check(chat, ack, senderId, { + chatType: ['group', 'channel'], + checkAdmin: true, + }); + if (!func) return func; - const chatMembers = chat.members; - const isCreator = chatMembers.some( - (member) => member.user.toString() === senderId && member.Role === 'admin' + await User.updateMany( + { _id: { $in: chat.members.map((m: any) => m.user) } }, + { $pull: { chats: { chat: chatId } } } ); - if (!isCreator) - return ack({ - success: false, - message: 'Could not delete the group', - error: 'you are not authorized to delete the group', - }); - - chatMembers.map(async (member: any) => { - await informSessions( - io, - member.user, - { chatId }, - 'DELETE_GROUP_CHANNEL_SERVER' - ); - }); + socket.to(chatId).emit('DELETE_GROUP_CHANNEL_SERVER', { chatId }); chat.members = []; chat.isDeleted = true; From 8c17b833c5c8585ba23362049b5c1aaa6228c0a2 Mon Sep 17 00:00:00 2001 From: AhmedHamed3699 Date: Sat, 21 Dec 2024 20:25:07 +0200 Subject: [PATCH 49/54] fix: solved filtering bug --- src/services/googleAIService.ts | 52 +++++++++++++++++--------------- src/sockets/MessagingServices.ts | 27 +++++++++-------- 2 files changed, 42 insertions(+), 37 deletions(-) diff --git a/src/services/googleAIService.ts b/src/services/googleAIService.ts index de0e7c0..b4af570 100644 --- a/src/services/googleAIService.ts +++ b/src/services/googleAIService.ts @@ -5,33 +5,37 @@ const hf = new HfInference(process.env.HF_API_KEY); const modelName = 'unitary/toxic-bert'; async function detectInappropriateContent(text: string): Promise { - try { - const response = await hf.textClassification({ - model: modelName, - inputs: text, - }); + try { + const response = await hf.textClassification({ + model: modelName, + inputs: text, + }); - console.log('Model Response:', JSON.stringify(response, null, 2)); + console.log('Model Response:', JSON.stringify(response, null, 2)); - const relevantLabels = ['toxic', 'obscene', 'insult', 'severe_toxic']; - const threshold = 0.7; + const relevantLabels = ['toxic', 'obscene', 'insult', 'severe_toxic']; + const threshold = 0.7; - interface TextClassificationResult { - label: string; - score: number; - } - - const toxicityScore = (response as TextClassificationResult[]) - .filter((result) => relevantLabels.includes(result.label.toLowerCase()) && result.score > threshold) - .reduce((acc, curr) => acc + curr.score, 0); - - console.log(`Total Toxicity Score: ${toxicityScore}`); - - return toxicityScore < threshold; - } catch (error) { - console.error('Error detecting inappropriate content:', error); - throw new Error('Failed to detect inappropriate content'); + interface TextClassificationResult { + label: string; + score: number; } + + const toxicityScore = (response as TextClassificationResult[]) + .filter( + (result) => + relevantLabels.includes(result.label.toLowerCase()) && + result.score > threshold + ) + .reduce((acc, curr) => acc + curr.score, 0); + + console.log(`Total Toxicity Score: ${toxicityScore}`); + + return toxicityScore >= threshold; + } catch (error) { + console.error('Error detecting inappropriate content:', error); + throw new Error('Failed to detect inappropriate content'); + } } -export default detectInappropriateContent; \ No newline at end of file +export default detectInappropriateContent; diff --git a/src/sockets/MessagingServices.ts b/src/sockets/MessagingServices.ts index ca076c8..c3301ae 100644 --- a/src/sockets/MessagingServices.ts +++ b/src/sockets/MessagingServices.ts @@ -58,17 +58,11 @@ export const check = async ( message: 'you do not have permission as you are not an admin', }); - if (sendMessage && sender.Role !== 'admin' && chat.type !== 'private') { + if (sendMessage && chat.type !== 'private') { const groupChannelChat = await GroupChannel.findById(chat._id); - - if (!groupChannelChat.messagingPermission) - return ack({ - success: false, - message: 'only admins can post and reply to this chat', - }); if ( chat?.type === 'group' && - !chat.isFilterd && + chat.isFilterd && (await detectInappropriateContent(content)) ) { return ack({ @@ -76,11 +70,18 @@ export const check = async ( message: 'inappropriate content', }); } - if (chat.type === 'channel' && !newMessageIsReply) - return ack({ - success: false, - message: 'only admins can post to this channel', - }); + if (sender.Role !== 'admin') { + if (!groupChannelChat.messagingPermission) + return ack({ + success: false, + message: 'only admins can post and reply to this chat', + }); + if (chat.type === 'channel' && !newMessageIsReply) + return ack({ + success: false, + message: 'only admins can post to this channel', + }); + } } return true; }; From 05f881acea777b7fe249f5ae9686f52510579d0c Mon Sep 17 00:00:00 2001 From: AhmedHamed3699 Date: Sat, 21 Dec 2024 21:48:26 +0200 Subject: [PATCH 50/54] fix: solved appropriate content bug --- src/controllers/userController.ts | 114 +++++++++++++++++++----------- src/models/messageModel.ts | 2 + src/sockets/MessagingServices.ts | 10 +-- src/sockets/messages.ts | 12 ++-- src/types/message.ts | 4 +- 5 files changed, 90 insertions(+), 52 deletions(-) diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts index 75b1e61..d99a0dc 100644 --- a/src/controllers/userController.ts +++ b/src/controllers/userController.ts @@ -14,22 +14,24 @@ interface GetUser extends Request { //TODO: add a user here that would contain the user data. } -export const getCurrentUser = catchAsync(async (req: GetUser, res: Response) => { - const userId = req.user.id; - - const user = await User.findById(userId); - - if (!user) { - throw new AppError('No User exists with this ID', 404); +export const getCurrentUser = catchAsync( + async (req: GetUser, res: Response) => { + const userId = req.user.id; + + const user = await User.findById(userId); + + if (!user) { + throw new AppError('No User exists with this ID', 404); + } + return res.status(200).json({ + status: 'success', + message: 'User retrieved successfuly', + data: { + user, + }, + }); } - return res.status(200).json({ - status: 'success', - message: 'User retrieved successfuly', - data: { - user, - }, - }); -}); +); export const updateCurrentUser = catchAsync(async (req: any, res: Response) => { const userData = req.body; @@ -84,7 +86,14 @@ export const getUser = catchAsync(async (req: GetUser, res: Response) => { throw new AppError('No User exists with this ID', 404); } - const fieldsToGet = ['username', 'screenFirstName', 'screenLastName', 'email', 'status', 'bio']; + const fieldsToGet = [ + 'username', + 'screenFirstName', + 'screenLastName', + 'email', + 'status', + 'bio', + ]; if ( user.picturePrivacy === 'everyone' || @@ -123,7 +132,11 @@ export const updateBio = catchAsync(async (req: any, res: Response) => { const { bio } = req.body; const userId = req.user.id; - const user = await User.findByIdAndUpdate(userId, { bio }, { new: true, runValidators: true }); + const user = await User.findByIdAndUpdate( + userId, + { bio }, + { new: true, runValidators: true } + ); if (!user) { throw new AppError('No User exists with this ID', 404); @@ -161,7 +174,11 @@ export const updateEmail = catchAsync(async (req: any, res: Response) => { const { email } = req.body; const userId = req.user.id; - const user = await User.findByIdAndUpdate(userId, { email }, { new: true, runValidators: true }); + const user = await User.findByIdAndUpdate( + userId, + { email }, + { new: true, runValidators: true } + ); if (!user) { throw new AppError('No User exists with this ID', 404); @@ -263,9 +280,9 @@ export const deletePicture = catchAsync(async (req: any, res: Response) => { data: {}, }); }); + export const getAllGroups = catchAsync(async (req: Request, res: Response) => { - const groupsAndChannels = await GroupChannel.find(); // Use `find()` in Mongoose to retrieve all documents - console.log(groupsAndChannels) + const groupsAndChannels = await GroupChannel.find(); return res.status(200).json({ status: 'success', message: 'Groups and Channels retrieved successfully', @@ -274,8 +291,23 @@ export const getAllGroups = catchAsync(async (req: Request, res: Response) => { }, }); }); + +export const toggleAutomaticDownload = catchAsync( + async (req: any, res: Response) => { + const userId = req.user.id; + const { enabled } = req.body; + + User.findByIdAndUpdate(userId, { automaticDownloadEnable: enabled }); + return res.status(200).json({ + status: 'success', + message: 'Automatic download settings updated successfully', + data: {}, + }); + } +); + export const activateUser = catchAsync(async (req: Request, res: Response) => { - const userId = req.params.userId; + const { userId } = req.params; const user = await User.findById(userId); if (!user) { @@ -284,7 +316,7 @@ export const activateUser = catchAsync(async (req: Request, res: Response) => { message: 'User not found', }); } -if (user.accountStatus === 'banned') { + if (user.accountStatus === 'banned') { return res.status(400).json({ status: 'fail', message: 'User is Banned', @@ -298,27 +330,29 @@ if (user.accountStatus === 'banned') { message: 'User activated successfully', }); }); -export const deactivateUser = catchAsync(async (req: Request, res: Response) => { - const userId = req.params.userId; - console.log(req.params) - const user = await User.findById(userId); - if (!user) { - return res.status(404).json({ - status: 'fail', - message: 'User not found', +export const deactivateUser = catchAsync( + async (req: Request, res: Response) => { + const { userId } = req.params; + console.log(req.params); + const user = await User.findById(userId); + if (!user) { + return res.status(404).json({ + status: 'fail', + message: 'User not found', + }); + } + + user.accountStatus = 'deactivated'; + await user.save(); + + return res.status(200).json({ + status: 'success', + message: 'User deactivated successfully', }); } - - user.accountStatus = 'deactivated'; - await user.save(); - - return res.status(200).json({ - status: 'success', - message: 'User deactivated successfully', - }); -}); +); export const banUser = catchAsync(async (req: Request, res: Response) => { - const userId = req.params.userId; + const { userId } = req.params; const user = await User.findById(userId); if (!user) { diff --git a/src/models/messageModel.ts b/src/models/messageModel.ts index 61e12a0..930c834 100644 --- a/src/models/messageModel.ts +++ b/src/models/messageModel.ts @@ -5,6 +5,8 @@ import Communication from './communicationModel'; const messageSchema = new mongoose.Schema({ content: String, media: String, + mediaName: String, + mediaSize: Number, contentType: { type: String, enum: ['text', 'image', 'GIF', 'sticker', 'audio', 'video', 'file', 'link'], diff --git a/src/sockets/MessagingServices.ts b/src/sockets/MessagingServices.ts index c3301ae..e35c42d 100644 --- a/src/sockets/MessagingServices.ts +++ b/src/sockets/MessagingServices.ts @@ -64,12 +64,8 @@ export const check = async ( chat?.type === 'group' && chat.isFilterd && (await detectInappropriateContent(content)) - ) { - return ack({ - success: false, - message: 'inappropriate content', - }); - } + ) + return 'inappropriate'; if (sender.Role !== 'admin') { if (!groupChannelChat.messagingPermission) return ack({ @@ -83,7 +79,7 @@ export const check = async ( }); } } - return true; + return 'ok'; }; export const informSessions = async ( diff --git a/src/sockets/messages.ts b/src/sockets/messages.ts index 9431798..9ace9fb 100644 --- a/src/sockets/messages.ts +++ b/src/sockets/messages.ts @@ -18,7 +18,8 @@ const handleMessaging = async ( ack: Function, senderId: string ) => { - let { media, content, contentType, parentMessageId } = data; + let { media, mediaName, mediaSize, content, contentType, parentMessageId } = + data; const { chatId, chatType, isReply, isForward, isAnnouncement } = data; if ( @@ -42,12 +43,12 @@ const handleMessaging = async ( }); const chat = await Chat.findById(chatId); - const func = await check(chat, ack, senderId, { + const valid = await check(chat, ack, senderId, { newMessageIsReply: isReply, content, sendMessage: true, }); - if (!func) return; + if (!valid) return; let parentMessage; if (isForward || isReply) { @@ -60,13 +61,15 @@ const handleMessaging = async ( }); if (isForward) { - ({ content, contentType, media } = parentMessage); + ({ content, contentType, media, mediaName, mediaSize } = parentMessage); parentMessageId = undefined; } } const message = new Message({ media, + mediaName, + mediaSize, content, contentType, isForward, @@ -74,6 +77,7 @@ const handleMessaging = async ( chatId, parentMessageId, isAnnouncement, + isAppropriate: valid === 'ok', }); await message.save(); diff --git a/src/types/message.ts b/src/types/message.ts index 0e4b363..470dd96 100644 --- a/src/types/message.ts +++ b/src/types/message.ts @@ -2,8 +2,10 @@ import { Types } from 'mongoose'; import ICommunication from './communication'; interface IMessage extends ICommunication { - media: string; content: string; + media: string; + mediaName: string; + mediaSize: number; contentType: string; isPinned: boolean; isForward: boolean; From 0fb3891756a6c50388997d4b30f5961bdba0f841 Mon Sep 17 00:00:00 2001 From: akramhany Date: Sat, 21 Dec 2024 01:10:49 +0200 Subject: [PATCH 51/54] feat(firebase): install firebase --- .env.example | 16 +- package-lock.json | 1773 +++++++++++++++++++++++++++++----------- package.json | 1 + src/config/firebase.ts | 11 + src/server.ts | 1 + 5 files changed, 1302 insertions(+), 500 deletions(-) create mode 100644 src/config/firebase.ts diff --git a/.env.example b/.env.example index 76a7ba4..b6527f5 100644 --- a/.env.example +++ b/.env.example @@ -44,4 +44,18 @@ GITHUB_CLIENT_SECRET=githu-client-secret CROSS_PLATFORM_OAUTH_REDIRECT_URL=telware://telware.online/social-auth-loading FRONTEND_URL=localhost:5174 -GROUP_SIZE= 5 \ No newline at end of file +GROUP_SIZE= 5 + +FIREBASE_SERVICE_ACCOUNT='{ + "type": "service_account", + "project_id": "your-project-id", + "private_key_id": "your-private-key-id", + "private_key": "-----BEGIN PRIVATE KEY-----\\nYOUR_PRIVATE_KEY\\n-----END PRIVATE KEY-----\\n", + "client_email": "your-client-email@your-project.iam.gserviceaccount.com", + "client_id": "your-client-id", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/your-client-email%40your-project.iam.gserviceaccount.com" + "universe_domain": "googleapis.com" +}' diff --git a/package-lock.json b/package-lock.json index 2dc05fc..94ee706 100644 --- a/package-lock.json +++ b/package-lock.json @@ -23,6 +23,7 @@ "express-mongo-sanitize": "^2.2.0", "express-rate-limit": "^7.4.1", "express-session": "^1.18.1", + "firebase-admin": "^13.0.2", "helmet": "^8.0.0", "hpp": "^0.2.3", "jsonwebtoken": "^9.0.2", @@ -1882,262 +1883,6 @@ "@jridgewell/sourcemap-codec": "^1.4.10" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.23.1.tgz", - "integrity": "sha512-6VhYk1diRqrhBAqpJEdjASR/+WVRtfjpqKuNw11cLiaWpAT/Uu+nokB+UJnevzy/P9C/ty6AOe0dwueMrGh/iQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.23.1.tgz", - "integrity": "sha512-uz6/tEy2IFm9RYOyvKl88zdzZfwEfKZmnX9Cj1BHjeSGNuGLuMD1kR8y5bteYmwqKm1tj8m4cb/aKEorr6fHWQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.23.1.tgz", - "integrity": "sha512-xw50ipykXcLstLeWH7WRdQuysJqejuAGPd30vd1i5zSyKK3WE+ijzHmLKxdiCMtH1pHz78rOg0BKSYOSB/2Khw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.23.1.tgz", - "integrity": "sha512-nlN9B69St9BwUoB+jkyU090bru8L0NA3yFvAd7k8dNsVH8bi9a8cUAUSEcEEgTp2z3dbEDGJGfP6VUnkQnlReg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.23.1.tgz", - "integrity": "sha512-YsS2e3Wtgnw7Wq53XXBLcV6JhRsEq8hkfg91ESVadIrzr9wO6jJDMZnCQbHm1Guc5t/CdDiFSSfWP58FNuvT3Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.23.1.tgz", - "integrity": "sha512-aClqdgTDVPSEGgoCS8QDG37Gu8yc9lTHNAQlsztQ6ENetKEO//b8y31MMu2ZaPbn4kVsIABzVLXYLhCGekGDqw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.23.1.tgz", - "integrity": "sha512-h1k6yS8/pN/NHlMl5+v4XPfikhJulk4G+tKGFIOwURBSFzE8bixw1ebjluLOjfwtLqY0kewfjLSrO6tN2MgIhA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.23.1.tgz", - "integrity": "sha512-lK1eJeyk1ZX8UklqFd/3A60UuZ/6UVfGT2LuGo3Wp4/z7eRTRYY+0xOu2kpClP+vMTi9wKOfXi2vjUpO1Ro76g==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.23.1.tgz", - "integrity": "sha512-CXXkzgn+dXAPs3WBwE+Kvnrf4WECwBdfjfeYHpMeVxWE0EceB6vhWGShs6wi0IYEqMSIzdOF1XjQ/Mkm5d7ZdQ==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.23.1.tgz", - "integrity": "sha512-/93bf2yxencYDnItMYV/v116zff6UyTjo4EtEQjUBeGiVpMmffDNUyD9UN2zV+V3LRV3/on4xdZ26NKzn6754g==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.23.1.tgz", - "integrity": "sha512-VTN4EuOHwXEkXzX5nTvVY4s7E/Krz7COC8xkftbbKRYAl96vPiUssGkeMELQMOnLOJ8k3BY1+ZY52tttZnHcXQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.23.1.tgz", - "integrity": "sha512-Vx09LzEoBa5zDnieH8LSMRToj7ir/Jeq0Gu6qJ/1GcBq9GkfoEAoXvLiW1U9J1qE/Y/Oyaq33w5p2ZWrNNHNEw==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.23.1.tgz", - "integrity": "sha512-nrFzzMQ7W4WRLNUOU5dlWAqa6yVeI0P78WKGUo7lg2HShq/yx+UYkeNSE0SSfSure0SqgnsxPvmAUu/vu0E+3Q==", - "cpu": [ - "mips64el" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.23.1.tgz", - "integrity": "sha512-dKN8fgVqd0vUIjxuJI6P/9SSSe/mB9rvA98CSH2sJnlZ/OCZWO1DJvxj8jvKTfYUdGfcq2dDxoKaC6bHuTlgcw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.23.1.tgz", - "integrity": "sha512-5AV4Pzp80fhHL83JM6LoA6pTQVWgB1HovMBsLQ9OZWLDqVY8MVobBXNSmAJi//Csh6tcY7e7Lny2Hg1tElMjIA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.23.1.tgz", - "integrity": "sha512-9ygs73tuFCe6f6m/Tb+9LtYxWR4c9yg7zjt2cYkjDbDpV/xVn+68cQxMXCjUpYwEkze2RcU/rMnfIXNRFmSoDw==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@esbuild/linux-x64": { "version": "0.23.1", "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.23.1.tgz", @@ -2154,118 +1899,6 @@ "node": ">=18" } }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.23.1.tgz", - "integrity": "sha512-aevEkCNu7KlPRpYLjwmdcuNz6bDFiE7Z8XC4CPqExjTvrHugh28QzUXVOZtiYghciKUacNktqxdpymplil1beA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.23.1.tgz", - "integrity": "sha512-3x37szhLexNA4bXhLrCC/LImN/YtWis6WXr1VESlfVtVeoFJBRINPJ3f0a/6LV8zpikqoUg4hyXw0sFBt5Cr+Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.23.1.tgz", - "integrity": "sha512-aY2gMmKmPhxfU+0EdnN+XNtGbjfQgwZj43k8G3fyrDM/UdZww6xrWxmDkuz2eCZchqVeABjV5BpildOrUbBTqA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.23.1.tgz", - "integrity": "sha512-RBRT2gqEl0IKQABT4XTj78tpk9v7ehp+mazn2HbUeZl1YMdaGAQqhapjGTCe7uw7y0frDi4gS0uHzhvpFuI1sA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.23.1.tgz", - "integrity": "sha512-4O+gPR5rEBe2FpKOVyiJ7wNDPA8nGzDuJ6gN4okSA1gEOYZ67N8JPk58tkWtdtPeLz7lBnY6I5L3jdsr3S+A6A==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.23.1.tgz", - "integrity": "sha512-BcaL0Vn6QwCwre3Y717nVHZbAa4UBEigzFm6VdsVdT/MbZ38xoj1X9HPkZhbmaBGUD1W8vxAfffbDe8bA6AKnQ==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.23.1", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.23.1.tgz", - "integrity": "sha512-BHpFFeslkWrXWyUPnbKm+xYYVYruCinGcftSBaa8zoF9hZO4BcSCFUvHVTtzpIY6YzUnYtuEhZ+C9iEXjxnasg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.1.tgz", @@ -2341,70 +1974,260 @@ "type-fest": "^0.20.2" }, "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/eslintrc/node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@faker-js/faker": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.3.0.tgz", + "integrity": "sha512-r0tJ3ZOkMd9xsu3VRfqlFR6cz0V/jFYRswAIpC+m/DIfAUXq7g8N7wTAlhSANySXYGKzGryfDXwtwsY8TxEIDw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/fakerjs" + } + ], + "engines": { + "node": ">=18.0.0", + "npm": ">=9.0.0" + } + }, + "node_modules/@fastify/busboy": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.1.0.tgz", + "integrity": "sha512-yHmUtGwEbW6HsKpPqT140/L6GpHtquHogRLgtanJFep3UAfDkE0fQfC49U+F9irCAoJVlv3M7VSp4rrtO4LnfA==" + }, + "node_modules/@firebase/app-check-interop-types": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@firebase/app-check-interop-types/-/app-check-interop-types-0.3.3.tgz", + "integrity": "sha512-gAlxfPLT2j8bTI/qfe3ahl2I2YcBQ8cFIBdhAQA4I2f3TndcO+22YizyGYuttLHPQEpWkhmpFW60VCFEPg4g5A==" + }, + "node_modules/@firebase/app-types": { + "version": "0.9.3", + "resolved": "https://registry.npmjs.org/@firebase/app-types/-/app-types-0.9.3.tgz", + "integrity": "sha512-kRVpIl4vVGJ4baogMDINbyrIOtOxqhkZQg4jTq3l8Lw6WSk0xfpEYzezFu+Kl4ve4fbPl79dvwRtaFqAC/ucCw==" + }, + "node_modules/@firebase/auth-interop-types": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@firebase/auth-interop-types/-/auth-interop-types-0.2.4.tgz", + "integrity": "sha512-JPgcXKCuO+CWqGDnigBtvo09HeBs5u/Ktc2GaFj2m01hLarbxthLNm7Fk8iOP1aqAtXV+fnnGj7U28xmk7IwVA==" + }, + "node_modules/@firebase/component": { + "version": "0.6.11", + "resolved": "https://registry.npmjs.org/@firebase/component/-/component-0.6.11.tgz", + "integrity": "sha512-eQbeCgPukLgsKD0Kw5wQgsMDX5LeoI1MIrziNDjmc6XDq5ZQnuUymANQgAb2wp1tSF9zDSXyxJmIUXaKgN58Ug==", + "dependencies": { + "@firebase/util": "1.10.2", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/database": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/@firebase/database/-/database-1.0.10.tgz", + "integrity": "sha512-sWp2g92u7xT4BojGbTXZ80iaSIaL6GAL0pwvM0CO/hb0nHSnABAqsH7AhnWGsGvXuEvbPr7blZylPaR9J+GSuQ==", + "dependencies": { + "@firebase/app-check-interop-types": "0.3.3", + "@firebase/auth-interop-types": "0.2.4", + "@firebase/component": "0.6.11", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.10.2", + "faye-websocket": "0.11.4", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/database-compat": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@firebase/database-compat/-/database-compat-2.0.1.tgz", + "integrity": "sha512-IsFivOjdE1GrjTeKoBU/ZMenESKDXidFDzZzHBPQ/4P20ptGdrl3oLlWrV/QJqJ9lND4IidE3z4Xr5JyfUW1vg==", + "dependencies": { + "@firebase/component": "0.6.11", + "@firebase/database": "1.0.10", + "@firebase/database-types": "1.0.7", + "@firebase/logger": "0.4.4", + "@firebase/util": "1.10.2", + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/database-types": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@firebase/database-types/-/database-types-1.0.7.tgz", + "integrity": "sha512-I7zcLfJXrM0WM+ksFmFdAMdlq/DFmpeMNa+/GNsLyFo5u/lX5zzkPzGe3srVWqaBQBY5KprylDGxOsP6ETfL0A==", + "dependencies": { + "@firebase/app-types": "0.9.3", + "@firebase/util": "1.10.2" + } + }, + "node_modules/@firebase/logger": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@firebase/logger/-/logger-0.4.4.tgz", + "integrity": "sha512-mH0PEh1zoXGnaR8gD1DeGeNZtWFKbnz9hDO91dIml3iou1gpOnLqXQ2dJfB71dj6dpmUjcQ6phY3ZZJbjErr9g==", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@firebase/util": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/@firebase/util/-/util-1.10.2.tgz", + "integrity": "sha512-qnSHIoE9FK+HYnNhTI8q14evyqbc/vHRivfB4TgCIUOl4tosmKSQlp7ltymOlMP4xVIJTg5wrkfcZ60X4nUf7Q==", + "dependencies": { + "tslib": "^2.1.0" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@eslint/eslintrc/node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "dev": true, + "node_modules/@google-cloud/firestore": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/@google-cloud/firestore/-/firestore-7.11.0.tgz", + "integrity": "sha512-88uZ+jLsp1aVMj7gh3EKYH1aulTAMFAp8sH/v5a9w8q8iqSG27RiWLoxSAFr/XocZ9hGiWH1kEnBw+zl3xAgNA==", + "optional": true, "dependencies": { - "argparse": "^2.0.1" + "@opentelemetry/api": "^1.3.0", + "fast-deep-equal": "^3.1.1", + "functional-red-black-tree": "^1.0.1", + "google-gax": "^4.3.3", + "protobufjs": "^7.2.6" }, - "bin": { - "js-yaml": "bin/js-yaml.js" + "engines": { + "node": ">=14.0.0" } }, - "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "node_modules/@google-cloud/paginator": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@google-cloud/paginator/-/paginator-5.0.2.tgz", + "integrity": "sha512-DJS3s0OVH4zFDB1PzjxAsHqJT6sKVbRwwML0ZBP9PbU7Yebtu/7SWMRzvO2J3nUi9pRNITCfu4LJeooM2w4pjg==", + "optional": true, "dependencies": { - "brace-expansion": "^1.1.7" + "arrify": "^2.0.0", + "extend": "^3.0.2" }, "engines": { - "node": "*" + "node": ">=14.0.0" } }, - "node_modules/@eslint/eslintrc/node_modules/type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true, + "node_modules/@google-cloud/projectify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-4.0.0.tgz", + "integrity": "sha512-MmaX6HeSvyPbWGwFq7mXdo0uQZLGBYCwziiLIGq5JVX+/bdI3SAq6bP98trV5eTWfLuvsMcIC1YJOF2vfteLFA==", + "optional": true, "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=14.0.0" } }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true, + "node_modules/@google-cloud/promisify": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-4.0.0.tgz", + "integrity": "sha512-Orxzlfb9c67A15cq2JQEyVc7wEsmFBmHjZWZYQMUyJ1qivXyMwdyNOs9odi79hze+2zqdTtu1E19IM/FtqZ10g==", + "optional": true, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node": ">=14" } }, - "node_modules/@faker-js/faker": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@faker-js/faker/-/faker-9.3.0.tgz", - "integrity": "sha512-r0tJ3ZOkMd9xsu3VRfqlFR6cz0V/jFYRswAIpC+m/DIfAUXq7g8N7wTAlhSANySXYGKzGryfDXwtwsY8TxEIDw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/fakerjs" - } - ], + "node_modules/@google-cloud/storage": { + "version": "7.14.0", + "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.14.0.tgz", + "integrity": "sha512-H41bPL2cMfSi4EEnFzKvg7XSb7T67ocSXrmF7MPjfgFB0L6CKGzfIYJheAZi1iqXjz6XaCT1OBf6HCG5vDBTOQ==", + "optional": true, + "dependencies": { + "@google-cloud/paginator": "^5.0.0", + "@google-cloud/projectify": "^4.0.0", + "@google-cloud/promisify": "^4.0.0", + "abort-controller": "^3.0.0", + "async-retry": "^1.3.3", + "duplexify": "^4.1.3", + "fast-xml-parser": "^4.4.1", + "gaxios": "^6.0.2", + "google-auth-library": "^9.6.3", + "html-entities": "^2.5.2", + "mime": "^3.0.0", + "p-limit": "^3.0.1", + "retry-request": "^7.0.0", + "teeny-request": "^9.0.0", + "uuid": "^8.0.0" + }, "engines": { - "node": ">=18.0.0", - "npm": ">=9.0.0" + "node": ">=14" + } + }, + "node_modules/@google-cloud/storage/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", + "optional": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@google-cloud/storage/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" } }, "node_modules/@google/generative-ai": { @@ -2415,6 +2238,37 @@ "node": ">=18.0.0" } }, + "node_modules/@grpc/grpc-js": { + "version": "1.12.5", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.12.5.tgz", + "integrity": "sha512-d3iiHxdpg5+ZcJ6jnDSOT8Z0O0VMVGy34jAnYLUX8yd36b1qn8f1TwOA/Lc7TsOh03IkPJ38eGI5qD2EjNkoEA==", + "optional": true, + "dependencies": { + "@grpc/proto-loader": "^0.7.13", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.7.13", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.7.13.tgz", + "integrity": "sha512-AiXO/bfe9bmxBjxxtYxFAXGZvMaN5s8kO+jBHAJCON8rJoB5YS/D6X7ZNc6XQkuHNmyl4CYaMI1fJ/Gn27RGGw==", + "optional": true, + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.2.5", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@huggingface/inference": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/@huggingface/inference/-/inference-2.8.1.tgz", @@ -2868,6 +2722,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "optional": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/@jsdevtools/ono": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", @@ -2993,6 +2857,15 @@ "node": ">=12.4.0" } }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "optional": true, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/@pkgr/core": { "version": "0.1.1", "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.1.1.tgz", @@ -3005,6 +2878,70 @@ "url": "https://opencollective.com/unts" } }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "optional": true + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "optional": true + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "optional": true + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "optional": true + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "optional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "optional": true + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "optional": true + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "optional": true + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "optional": true + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "optional": true + }, "node_modules/@redis/bloom": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-1.2.0.tgz", @@ -3155,6 +3092,15 @@ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==" }, + "node_modules/@tootallnate/once": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", + "integrity": "sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A==", + "optional": true, + "engines": { + "node": ">= 10" + } + }, "node_modules/@tsconfig/node10": { "version": "1.0.11", "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.11.tgz", @@ -3243,17 +3189,21 @@ "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", - "dev": true, "dependencies": { "@types/connect": "*", "@types/node": "*" } }, + "node_modules/@types/caseless": { + "version": "0.12.5", + "resolved": "https://registry.npmjs.org/@types/caseless/-/caseless-0.12.5.tgz", + "integrity": "sha512-hWtVTC2q7hc7xZ/RLbxapMvDMgUnDvKvMOpKal4DrMyfGBUfB1oKaZlIRr6mJL+If3bAP6sV/QneGzF6tJjZDg==", + "optional": true + }, "node_modules/@types/connect": { "version": "3.4.38", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "dev": true, "dependencies": { "@types/node": "*" } @@ -3334,8 +3284,7 @@ "node_modules/@types/http-errors": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", - "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", - "dev": true + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==" }, "node_modules/@types/istanbul-lib-coverage": { "version": "2.0.6", @@ -3386,11 +3335,16 @@ "version": "9.0.7", "resolved": "https://registry.npmjs.org/@types/jsonwebtoken/-/jsonwebtoken-9.0.7.tgz", "integrity": "sha512-ugo316mmTYBl2g81zDFnZ7cfxlut3o+/EQdaP7J8QN2kY6lJ22hmQYCK5EHcJHbrW+dkCGSCPgbG8JtYj6qSrg==", - "dev": true, "dependencies": { "@types/node": "*" } }, + "node_modules/@types/long": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz", + "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA==", + "optional": true + }, "node_modules/@types/mdast": { "version": "4.0.4", "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", @@ -3403,8 +3357,7 @@ "node_modules/@types/mime": { "version": "1.3.5", "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "dev": true + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==" }, "node_modules/@types/morgan": { "version": "1.9.9", @@ -3495,20 +3448,44 @@ "node_modules/@types/qs": { "version": "6.9.17", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.17.tgz", - "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==", - "dev": true + "integrity": "sha512-rX4/bPcfmvxHDv0XjfJELTTr+iB+tn032nPILqHm5wbthUUUuVtNGGqzhya9XUxjTP8Fpr0qYgSZZKxGY++svQ==" }, "node_modules/@types/range-parser": { "version": "1.2.7", "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "dev": true + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" + }, + "node_modules/@types/request": { + "version": "2.48.12", + "resolved": "https://registry.npmjs.org/@types/request/-/request-2.48.12.tgz", + "integrity": "sha512-G3sY+NpsA9jnwm0ixhAFQSJ3Q9JkpLZpJbI3GMv0mIAT0y3mRabYeINzal5WOChIiaTEGQYlHOKgkaM9EisWHw==", + "optional": true, + "dependencies": { + "@types/caseless": "*", + "@types/node": "*", + "@types/tough-cookie": "*", + "form-data": "^2.5.0" + } + }, + "node_modules/@types/request/node_modules/form-data": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.5.2.tgz", + "integrity": "sha512-GgwY0PS7DbXqajuGf4OYlsrIu3zgxD6Vvql43IBhm6MahqA5SK/7mwhtNj2AdH2z35YR34ujJ7BN+3fFC3jP5Q==", + "optional": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } }, "node_modules/@types/send": { "version": "0.17.4", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", - "dev": true, "dependencies": { "@types/mime": "^1", "@types/node": "*" @@ -3518,7 +3495,6 @@ "version": "1.15.7", "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.7.tgz", "integrity": "sha512-W8Ym+h8nhuRwaKPaDw34QUkwsGi6Rc4yYqvKFo5rm2FUEhCFbzVWrxXUxuKK8TASjWsysJY0nsmNCGhCOIsrOw==", - "dev": true, "dependencies": { "@types/http-errors": "*", "@types/node": "*", @@ -3559,6 +3535,12 @@ "@types/serve-static": "*" } }, + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", + "optional": true + }, "node_modules/@types/ua-parser-js": { "version": "0.7.39", "resolved": "https://registry.npmjs.org/@types/ua-parser-js/-/ua-parser-js-0.7.39.tgz", @@ -3818,6 +3800,18 @@ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "optional": true, + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -3916,6 +3910,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "devOptional": true, "dependencies": { "color-convert": "^2.0.1" }, @@ -4161,6 +4156,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/arrify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-2.0.1.tgz", + "integrity": "sha512-3duEwti880xqi4eAMN8AyR4a0ByT90zoYdLlevfrvU43vb0YZwZVfxOgxWrLXXXpyugL0hNZc9G6BiB5B3nUug==", + "optional": true, + "engines": { + "node": ">=8" + } + }, "node_modules/asap": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", @@ -4194,6 +4198,15 @@ "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", "dev": true }, + "node_modules/async-retry": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/async-retry/-/async-retry-1.3.3.tgz", + "integrity": "sha512-wfr/jstw9xNi/0teMHrRW7dsz3Lt5ARhYNZ2ewpadnhaIp5mbALhOAP+EAdsC7t4Z6wqsDVv9+W6gm1Dk9mEyw==", + "optional": true, + "dependencies": { + "retry": "0.13.1" + } + }, "node_modules/asynckit": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", @@ -4393,6 +4406,25 @@ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==" }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, "node_modules/base64id": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", @@ -4446,6 +4478,14 @@ "tweetnacl": "^0.14.3" } }, + "node_modules/bignumber.js": { + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.1.2.tgz", + "integrity": "sha512-2/mKyZH9K85bzOEfhXDBFZTGd1CTs+5IHpeFQo9luiBG7hghdC851Pj2WAhb6E3R6b9tZj/XKhbg4fum+Kepug==", + "engines": { + "node": "*" + } + }, "node_modules/binary-extensions": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", @@ -4498,6 +4538,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, "dependencies": { "balanced-match": "^1.0.0" } @@ -4711,6 +4752,7 @@ "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" @@ -4799,6 +4841,7 @@ "version": "3.9.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", + "dev": true, "funding": [ { "type": "github", @@ -4834,7 +4877,7 @@ "version": "8.0.1", "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", - "dev": true, + "devOptional": true, "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", @@ -4872,6 +4915,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "devOptional": true, "dependencies": { "color-name": "~1.1.4" }, @@ -4882,7 +4926,8 @@ "node_modules/color-name": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "devOptional": true }, "node_modules/color-support": { "version": "1.1.3", @@ -5445,6 +5490,32 @@ "node": ">= 0.4" } }, + "node_modules/duplexify": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-4.1.3.tgz", + "integrity": "sha512-M3BmBhwJRZsSx38lZyhE53Csddgzl5R7xGJNk7CVddZD6CcmwMCH8J+7AprIrQKH7TonKxaCjcv27Qmf+sQ+oA==", + "optional": true, + "dependencies": { + "end-of-stream": "^1.4.1", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1", + "stream-shift": "^1.0.2" + } + }, + "node_modules/duplexify/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/dynamic-dedupe": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/dynamic-dedupe/-/dynamic-dedupe-0.3.0.tgz", @@ -5529,6 +5600,15 @@ "node": ">= 0.8" } }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "optional": true, + "dependencies": { + "once": "^1.4.0" + } + }, "node_modules/engine.io": { "version": "6.6.2", "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.2.tgz", @@ -5813,7 +5893,7 @@ "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "dev": true, + "devOptional": true, "engines": { "node": ">=6" } @@ -6639,6 +6719,15 @@ "node": ">= 0.6" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "optional": true, + "engines": { + "node": ">=6" + } + }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -6824,6 +6913,14 @@ "node >=0.6.0" ] }, + "node_modules/farmhash-modern": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/farmhash-modern/-/farmhash-modern-1.1.0.tgz", + "integrity": "sha512-6ypT4XfgqJk/F3Yuv4SX26I3doUjt0GTG4a+JgWxXQpxXzTBq8fPUeGHfcYMMDPHJHm3yPOSjaeBwBGAHWXCdA==", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6879,6 +6976,28 @@ "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", "integrity": "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==" }, + "node_modules/fast-xml-parser": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.1.tgz", + "integrity": "sha512-y655CeyUQ+jj7KBbYMc4FG01V8ZQqjN+gDYGJ50RtfsUB8iG9AmwmwoAgeKLJdmueKKMrH1RJ7yXHTSoczdv5w==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + }, + { + "type": "paypal", + "url": "https://paypal.me/naturalintelligence" + } + ], + "optional": true, + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.17.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.17.1.tgz", @@ -6888,6 +7007,17 @@ "reusify": "^1.0.4" } }, + "node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/fb-watchman": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", @@ -7007,6 +7137,42 @@ "node": ">=8" } }, + "node_modules/firebase-admin": { + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/firebase-admin/-/firebase-admin-13.0.2.tgz", + "integrity": "sha512-YWVpoN+tZVSRXF0qC0gojoF5bSqvBRbnBk8+xUtFiguM2L4vB7f0moAwV1VVWDDHvTnvQ68OyTMpdp6wKo/clw==", + "dependencies": { + "@fastify/busboy": "^3.0.0", + "@firebase/database-compat": "^2.0.0", + "@firebase/database-types": "^1.0.6", + "@types/node": "^22.8.7", + "farmhash-modern": "^1.1.0", + "google-auth-library": "^9.14.2", + "jsonwebtoken": "^9.0.0", + "jwks-rsa": "^3.1.0", + "node-forge": "^1.3.1", + "uuid": "^11.0.2" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@google-cloud/firestore": "^7.11.0", + "@google-cloud/storage": "^7.14.0" + } + }, + "node_modules/firebase-admin/node_modules/uuid": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.0.3.tgz", + "integrity": "sha512-d0z310fCWv5dJwnX1Y/MncBAqGMKEzlBb1AOf7z9K8ALnd0utBX/msg/fA0+sbyN1ihbMsLhrBlnl1ak7Wa0rg==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/esm/bin/uuid" + } + }, "node_modules/flat-cache": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", @@ -7171,20 +7337,6 @@ "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==" }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "dev": true, - "hasInstallScript": true, - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -7212,6 +7364,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/functional-red-black-tree": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", + "integrity": "sha512-dsKNQNdj6xA3T+QlADDA7mOSlX0qiMINjn0cgr+eGHGsbSHzTabcIogz2+p/iqP1Xs6EP/sS2SbqH+brGTbq0g==", + "optional": true + }, "node_modules/functions-have-names": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", @@ -7241,6 +7399,167 @@ "node": ">=10" } }, + "node_modules/gaxios": { + "version": "6.7.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz", + "integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/gaxios/node_modules/agent-base": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.3.tgz", + "integrity": "sha512-jRR5wdylq8CkOe6hei19GGZnxM6rBGwFl3Bg0YItGDimvjGtAvdZk4Pu6Cl4u4Igsws4a1fd1Vq3ezrhn4KmFw==", + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/gaxios/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/gaxios/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, + "node_modules/gaxios/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/gaxios/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/gaxios/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/gcp-metadata": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-5.3.0.tgz", + "integrity": "sha512-FNTkdNEnBdlqF2oatizolQqNANMrcqJt6AAYt99B3y1aLLC8Hc5IOBb+ZnnzllodEEf6xMBp6wRcBbc16fa65w==", + "optional": true, + "peer": true, + "dependencies": { + "gaxios": "^5.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gcp-metadata/node_modules/gaxios": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-5.1.3.tgz", + "integrity": "sha512-95hVgBRgEIRQQQHIbnxBXeHbW4TqFk4ZDJW7wmVtvYar72FdhRIo1UGOLS2eRAKCPEdPBWu+M7+A33D9CdX9rA==", + "optional": true, + "peer": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^5.0.0", + "is-stream": "^2.0.0", + "node-fetch": "^2.6.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/gcp-metadata/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "optional": true, + "peer": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/gcp-metadata/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "optional": true, + "peer": true + }, + "node_modules/gcp-metadata/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "optional": true, + "peer": true + }, + "node_modules/gcp-metadata/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "optional": true, + "peer": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/generic-pool": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/generic-pool/-/generic-pool-3.9.0.tgz", @@ -7262,7 +7581,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, + "devOptional": true, "engines": { "node": "6.* || 8.* || >= 10.*" } @@ -7448,6 +7767,131 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/google-auth-library": { + "version": "9.15.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.0.tgz", + "integrity": "sha512-7ccSEJFDFO7exFbO6NRyC+xH8/mZ1GZGG2xxx9iHxZWcjUjJpjWxIMw3cofAKcueZ6DATiukmmprD7yavQHOyQ==", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^6.1.1", + "gcp-metadata": "^6.1.0", + "gtoken": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-auth-library/node_modules/gcp-metadata": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.0.tgz", + "integrity": "sha512-Jh/AIwwgaxan+7ZUUmRLCjtchyDiqh4KjBJ5tW3plBZb5iL/BPcso8A5DlzeD9qlw0duCamnNdpFjxwaT0KyKg==", + "dependencies": { + "gaxios": "^6.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-auth-library/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/google-auth-library/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/google-gax": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-4.4.1.tgz", + "integrity": "sha512-Phyp9fMfA00J3sZbJxbbB4jC55b7DBjE3F6poyL3wKMEBVKA79q6BGuHcTiM28yOzVql0NDbRL8MLLh8Iwk9Dg==", + "optional": true, + "dependencies": { + "@grpc/grpc-js": "^1.10.9", + "@grpc/proto-loader": "^0.7.13", + "@types/long": "^4.0.0", + "abort-controller": "^3.0.0", + "duplexify": "^4.0.0", + "google-auth-library": "^9.3.0", + "node-fetch": "^2.7.0", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^2.0.2", + "protobufjs": "^7.3.2", + "retry-request": "^7.0.0", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/google-gax/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/google-gax/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "optional": true + }, + "node_modules/google-gax/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/google-gax/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "optional": true + }, + "node_modules/google-gax/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -7462,7 +7906,8 @@ "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true }, "node_modules/graphemer": { "version": "1.4.0", @@ -7470,6 +7915,37 @@ "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", "dev": true }, + "node_modules/gtoken": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", + "integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==", + "dependencies": { + "gaxios": "^6.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/gtoken/node_modules/jwa": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", + "integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==", + "dependencies": { + "buffer-equal-constant-time": "1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/gtoken/node_modules/jws": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz", + "integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==", + "dependencies": { + "jwa": "^2.0.0", + "safe-buffer": "^5.0.1" + } + }, "node_modules/har-schema": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", @@ -7507,6 +7983,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, "engines": { "node": ">=8" } @@ -7644,6 +8121,22 @@ "node": ">=0.10.0" } }, + "node_modules/html-entities": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.5.2.tgz", + "integrity": "sha512-K//PSRMQk4FZ78Kyau+mZurHn3FH0Vwr+H36eE0rPbeYkRRi9YxceYPhuN60UwWorxyKHhqoAJl2OFKa4BVtaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/mdevils" + }, + { + "type": "patreon", + "url": "https://patreon.com/mdevils" + } + ], + "optional": true + }, "node_modules/html-escaper": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", @@ -7675,6 +8168,25 @@ "node": ">= 0.8" } }, + "node_modules/http-parser-js": { + "version": "0.5.8", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.8.tgz", + "integrity": "sha512-SGeBX54F94Wgu5RH3X5jsDtf4eHyRogWX1XGT3b4HuW3tQPM4AaBzoUji/4AAJNXCEOWZ5O0DgZmJw1947gD5Q==" + }, + "node_modules/http-proxy-agent": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-5.0.0.tgz", + "integrity": "sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==", + "optional": true, + "dependencies": { + "@tootallnate/once": "2", + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/http-signature": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", @@ -7784,6 +8296,7 @@ "version": "0.1.4", "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, "engines": { "node": ">=0.8.19" } @@ -8162,7 +8675,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true, "engines": { "node": ">=8" }, @@ -8274,7 +8786,8 @@ "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true }, "node_modules/isstream": { "version": "0.1.2", @@ -8998,6 +9511,14 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, + "node_modules/jose": { + "version": "4.15.9", + "resolved": "https://registry.npmjs.org/jose/-/jose-4.15.9.tgz", + "integrity": "sha512-1vUQX+IdDMVPj4k8kOxgUqlcK518yluMuGZwqlr44FS1ppZB/5GWh4rZG89erpOBOJjU/OBsnCVFfapsRz6nEA==", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -9034,6 +9555,14 @@ "node": ">=6" } }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", @@ -9043,7 +9572,8 @@ "node_modules/json-parse-even-better-errors": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true }, "node_modules/json-schema": { "version": "0.4.0", @@ -9161,6 +9691,44 @@ "safe-buffer": "^5.0.1" } }, + "node_modules/jwks-rsa": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jwks-rsa/-/jwks-rsa-3.1.0.tgz", + "integrity": "sha512-v7nqlfezb9YfHHzYII3ef2a2j1XnGeSE/bK3WfumaYCqONAIstJbrEGapz4kadScZzEt7zYCN7bucj8C0Mv/Rg==", + "dependencies": { + "@types/express": "^4.17.17", + "@types/jsonwebtoken": "^9.0.2", + "debug": "^4.3.4", + "jose": "^4.14.6", + "limiter": "^1.1.5", + "lru-memoizer": "^2.2.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/jwks-rsa/node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } + }, + "node_modules/jwks-rsa/node_modules/@types/express-serve-static-core": { + "version": "4.19.6", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", + "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", + "dependencies": { + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" + } + }, "node_modules/jws": { "version": "3.2.2", "resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz", @@ -9236,6 +9804,11 @@ "node": ">= 0.8.0" } }, + "node_modules/limiter": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/limiter/-/limiter-1.1.5.tgz", + "integrity": "sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==" + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -9268,6 +9841,17 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "optional": true + }, + "node_modules/lodash.clonedeep": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", + "integrity": "sha512-H5ZhCF25riFd9uB5UCkVKo61m3S/xZk1x4wA6yp/L3RFP6Z/eHH1ymQcGLo7J3GMPfm0V/7m1tryHuGVxpqEBQ==" + }, "node_modules/lodash.debounce": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", @@ -9336,6 +9920,12 @@ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz", "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==" }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==", + "optional": true + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -9357,6 +9947,31 @@ "yallist": "^3.0.2" } }, + "node_modules/lru-memoizer": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lru-memoizer/-/lru-memoizer-2.3.0.tgz", + "integrity": "sha512-GXn7gyHAMhO13WSKrIiNfztwxodVsP8IoZ3XfrJV4yH2x0/OeTO/FIaAHTY5YekdGgW94njfuKmyyt1E0mR6Ug==", + "dependencies": { + "lodash.clonedeep": "^4.5.0", + "lru-cache": "6.0.0" + } + }, + "node_modules/lru-memoizer/node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/lru-memoizer/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + }, "node_modules/lunr": { "version": "2.3.9", "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", @@ -9663,6 +10278,7 @@ "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, "dependencies": { "brace-expansion": "^2.0.1" }, @@ -9955,6 +10571,14 @@ "url": "https://opencollective.com/node-fetch" } }, + "node_modules/node-forge": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", + "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "engines": { + "node": ">= 6.13.0" + } + }, "node_modules/node-int64": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", @@ -12663,6 +13287,15 @@ "node": ">=0.10.0" } }, + "node_modules/object-hash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-3.0.0.tgz", + "integrity": "sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==", + "optional": true, + "engines": { + "node": ">= 6" + } + }, "node_modules/object-inspect": { "version": "1.13.3", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.3.tgz", @@ -12859,7 +13492,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, + "devOptional": true, "dependencies": { "yocto-queue": "^0.1.0" }, @@ -13233,6 +13866,42 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/proto3-json-serializer": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-2.0.2.tgz", + "integrity": "sha512-SAzp/O4Yh02jGdRc+uIrGoe87dkN/XtwxfZ4ZyafJHymd79ozp5VG5nyZ7ygqPM5+cpLDjjGnYFUkngonyDPOQ==", + "optional": true, + "dependencies": { + "protobufjs": "^7.2.5" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/protobufjs": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.4.0.tgz", + "integrity": "sha512-mRUWCc3KUU4w1jU8sGxICXH/gNS94DvI1gxqDvBzhj1JpcsimQkYiOJfwsPUykUI5ZaspFbSgmBLER8IrQ3tqw==", + "hasInstallScript": true, + "optional": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.4", + "@protobufjs/eventemitter": "^1.1.0", + "@protobufjs/fetch": "^1.1.0", + "@protobufjs/float": "^1.0.2", + "@protobufjs/inquire": "^1.1.0", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.0", + "@types/node": ">=13.7.0", + "long": "^5.0.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/proxy-addr": { "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", @@ -13635,7 +14304,7 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, + "devOptional": true, "engines": { "node": ">=0.10.0" } @@ -13699,6 +14368,29 @@ "node": ">=10" } }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "optional": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/retry-request": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-7.0.2.tgz", + "integrity": "sha512-dUOvLMJ0/JJYEn8NrpOaGNE7X3vpI5XlZS/u0ANjqtcZVKnIxP7IgCFwrKTxENw29emmwug53awKtaMm4i9g5w==", + "optional": true, + "dependencies": { + "@types/request": "^2.48.8", + "extend": "^3.0.2", + "teeny-request": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -14281,6 +14973,21 @@ "node": ">= 0.8" } }, + "node_modules/stream-events": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/stream-events/-/stream-events-1.0.5.tgz", + "integrity": "sha512-E1GUzBSgvct8Jsb3v2X15pjzN1tYebtbLaMg+eBOUOAxgbLoSbT2NS91ckc5lJD1KfLjId+jXJRgo0qnV5Nerg==", + "optional": true, + "dependencies": { + "stubs": "^3.0.0" + } + }, + "node_modules/stream-shift": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.3.tgz", + "integrity": "sha512-76ORR0DO1o1hlKwTbi/DM3EXWGf3ZJYO8cXX5RJwnul2DEg2oyoZyjLNoQM8WsvZiFKCRfC1O0J7iCvie3RZmQ==", + "optional": true + }, "node_modules/streamsearch": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", @@ -14494,6 +15201,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", + "optional": true + }, + "node_modules/stubs": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/stubs/-/stubs-3.0.0.tgz", + "integrity": "sha512-PdHt7hHUJKxvTCgbKX9C1V/ftOcjJQgz8BZwNfV5c4B6dcGqlpelTbJ999jBGZ2jYiPAwcX5dP6oBwVlBlUbxw==", + "optional": true + }, "node_modules/superagent": { "version": "9.0.2", "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", @@ -14540,6 +15259,7 @@ "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { "has-flag": "^4.0.0" }, @@ -14716,49 +15436,81 @@ "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" }, - "node_modules/telware-backend": { - "version": "1.0.0", - "resolved": "file:", - "license": "ISC", + "node_modules/teeny-request": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-9.0.0.tgz", + "integrity": "sha512-resvxdc6Mgb7YEThw6G6bExlXKkv6+YbuzGg9xuXxSgxJF7Ozs+o8Y9+2R3sArdWdW8nOokoQb1yrpFB0pQK2g==", + "optional": true, "dependencies": { - "@faker-js/faker": "^9.0.3", - "axios": "^1.7.7", - "bcrypt": "^5.1.1", - "body-parser": "^1.20.3", - "connect-redis": "^7.1.1", - "cors": "^2.8.5", - "crypto": "^1.0.1", - "dotenv": "^16.4.5", - "express": "^4.21.1", - "express-mongo-sanitize": "^2.2.0", - "express-rate-limit": "^7.4.1", - "express-session": "^1.18.1", - "helmet": "^8.0.0", - "hpp": "^0.2.3", - "jsonwebtoken": "^9.0.2", - "module-alias": "^2.2.3", - "mongodb": "^6.11.0", - "mongoose": "^8.7.2", - "morgan": "^1.10.0", - "multer": "^1.4.5-lts.1", - "node-fetch": "^3.3.2", - "nodemailer": "^6.9.15", - "npm": "^10.9.1", - "passport": "^0.7.0", - "passport-github2": "^0.1.12", - "passport-google-oauth20": "^2.0.0", - "redis": "^4.7.0", - "request": "^2.88.2", - "socket.io": "^4.8.1", - "supertest": "^7.0.0", - "swagger-jsdoc": "^6.2.8", - "swagger-ui-express": "^5.0.1", - "telware-backend": "file:", - "ua-parser-js": "^1.0.39", - "validator": "^13.12.0", - "yamljs": "^0.3.0" + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^2.6.9", + "stream-events": "^1.0.5", + "uuid": "^9.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/teeny-request/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/teeny-request/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "optional": true + }, + "node_modules/teeny-request/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/teeny-request/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "optional": true + }, + "node_modules/teeny-request/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" } }, + "node_modules/telware-backend": { + "resolved": "", + "link": true + }, "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", @@ -14798,7 +15550,8 @@ "node_modules/text-table": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==" + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true }, "node_modules/tmpl": { "version": "1.0.5", @@ -15124,8 +15877,7 @@ "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "dev": true + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" }, "node_modules/tsx": { "version": "4.19.2", @@ -15723,6 +16475,27 @@ "node": ">=12" } }, + "node_modules/websocket-driver": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", + "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "dependencies": { + "http-parser-js": ">=0.5.1", + "safe-buffer": ">=5.1.0", + "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/websocket-extensions": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", + "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/whatwg-url": { "version": "13.0.0", "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", @@ -15739,6 +16512,7 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, "dependencies": { "isexe": "^2.0.0" }, @@ -15860,7 +16634,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "dev": true, + "devOptional": true, "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -15882,6 +16656,7 @@ "version": "4.0.2", "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", + "dev": true, "dependencies": { "imurmurhash": "^0.1.4", "signal-exit": "^3.0.7" @@ -15922,7 +16697,7 @@ "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", - "dev": true, + "devOptional": true, "engines": { "node": ">=10" } @@ -15958,7 +16733,7 @@ "version": "17.7.2", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", - "dev": true, + "devOptional": true, "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", @@ -15976,7 +16751,7 @@ "version": "21.1.1", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", - "dev": true, + "devOptional": true, "engines": { "node": ">=12" } @@ -15994,7 +16769,7 @@ "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true, + "devOptional": true, "engines": { "node": ">=10" }, diff --git a/package.json b/package.json index 5053f44..3e4faf8 100644 --- a/package.json +++ b/package.json @@ -92,6 +92,7 @@ "express-mongo-sanitize": "^2.2.0", "express-rate-limit": "^7.4.1", "express-session": "^1.18.1", + "firebase-admin": "^13.0.2", "helmet": "^8.0.0", "hpp": "^0.2.3", "jsonwebtoken": "^9.0.2", diff --git a/src/config/firebase.ts b/src/config/firebase.ts new file mode 100644 index 0000000..a47780d --- /dev/null +++ b/src/config/firebase.ts @@ -0,0 +1,11 @@ +import admin from 'firebase-admin'; + +const serviceAccount = JSON.parse( + process.env.FIREBASE_SERVICE_ACCOUNT as string +); + +admin.initializeApp({ + credential: admin.credential.cert(serviceAccount), +}); + +export default admin; diff --git a/src/server.ts b/src/server.ts index 3c93c46..2828727 100644 --- a/src/server.ts +++ b/src/server.ts @@ -1,6 +1,7 @@ import http from 'http'; import '@config/env'; import '@config/passport'; +import '@config/firebase'; import mongoDBConnection from '@config/mongoDB'; import app from '@base/app'; import socketSetup from './sockets/socket'; From cb2b33154b8613aa935fb9a5f4e2feb824331d99 Mon Sep 17 00:00:00 2001 From: akramhany Date: Sat, 21 Dec 2024 18:31:58 +0200 Subject: [PATCH 52/54] feat(fcmToken): implement an endpoint to update the FCM token --- docs/api/user.swagger.ts | 68 +++++++++++++++++++++++++++++++ src/controllers/userController.ts | 27 +++++++++++- src/models/userModel.ts | 16 +++++--- src/routes/userRoute.ts | 15 +++---- src/types/user.ts | 1 + 5 files changed, 112 insertions(+), 15 deletions(-) diff --git a/docs/api/user.swagger.ts b/docs/api/user.swagger.ts index 461c91f..3cb2630 100644 --- a/docs/api/user.swagger.ts +++ b/docs/api/user.swagger.ts @@ -1416,3 +1416,71 @@ * - Session not found, you are not allowed here! * - You are not authorized to view these stories */ + +/** + * @swagger + * /users/fcm-token: + * patch: + * summary: Update the FCM token of the authenticated user + * description: This endpoint allows an authenticated user to update their Firebase Cloud Messaging (FCM) token. + * tags: + * - User + * security: + * - cookieAuth: [] + * requestBody: + * required: true + * content: + * application/json: + * schema: + * type: object + * properties: + * fcmToken: + * type: string + * description: The FCM token to be updated. + * example: "aslkdfadslfdaskf" + * required: + * - fcmToken + * responses: + * 201: + * description: FCM token updated successfully. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: "success" + * message: + * type: string + * example: "User fcm token updated successfully" + * data: + * type: object + * example: {} + * 404: + * description: No user exists with the given ID. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: "error" + * message: + * type: string + * example: "No User exists with this ID" + * 500: + * description: Internal server error. + * content: + * application/json: + * schema: + * type: object + * properties: + * status: + * type: string + * example: "error" + * message: + * type: string + * example: "Internal Server Error" + */ diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts index d99a0dc..1ca3045 100644 --- a/src/controllers/userController.ts +++ b/src/controllers/userController.ts @@ -282,7 +282,8 @@ export const deletePicture = catchAsync(async (req: any, res: Response) => { }); export const getAllGroups = catchAsync(async (req: Request, res: Response) => { - const groupsAndChannels = await GroupChannel.find(); + const groupsAndChannels = await GroupChannel.find(); // Use `find()` in Mongoose to retrieve all documents + return res.status(200).json({ status: 'success', message: 'Groups and Channels retrieved successfully', @@ -330,10 +331,10 @@ export const activateUser = catchAsync(async (req: Request, res: Response) => { message: 'User activated successfully', }); }); + export const deactivateUser = catchAsync( async (req: Request, res: Response) => { const { userId } = req.params; - console.log(req.params); const user = await User.findById(userId); if (!user) { return res.status(404).json({ @@ -351,6 +352,7 @@ export const deactivateUser = catchAsync( }); } ); + export const banUser = catchAsync(async (req: Request, res: Response) => { const { userId } = req.params; @@ -370,3 +372,24 @@ export const banUser = catchAsync(async (req: Request, res: Response) => { message: 'User banned successfully', }); }); + +export const updateFCMToken = catchAsync(async (req: any, res: Response) => { + const userId = req.user.id; + const { fcmToken } = req.body; + + const user = await User.findOneAndUpdate( + { _id: userId }, + { fcmToken }, + { new: true, runValidators: true } + ); + + if (!user) { + throw new AppError('No User exists with this ID', 404); + } + + return res.status(201).json({ + status: 'success', + message: 'User fcm token updated successfuly', + data: {}, + }); +}); diff --git a/src/models/userModel.ts b/src/models/userModel.ts index a797111..4fa9a4c 100644 --- a/src/models/userModel.ts +++ b/src/models/userModel.ts @@ -30,6 +30,10 @@ const userSchema = new mongoose.Schema( message: 'Username can contain only letters, numbers and underscore', }, }, + fcmToken: { + type: String, + default: '', + }, screenFirstName: { type: String, default: '', @@ -235,21 +239,21 @@ const userSchema = new mongoose.Schema( userSchema.index({ email: 1 }, { background: true }); -userSchema.pre('save', async function (next) { +userSchema.pre('save', async function(next) { if (!this.isModified('password') || !this.password) return next(); this.password = await bcrypt.hash(this.password, 12); this.passwordConfirm = undefined; next(); }); -userSchema.pre('save', function (next) { +userSchema.pre('save', function(next) { if (this.provider === 'local') { this.providerId = this._id as string; } next(); }); -userSchema.methods.isCorrectPassword = async function ( +userSchema.methods.isCorrectPassword = async function( candidatePass: string ): Promise { const result = await bcrypt.compare(candidatePass, this.password); @@ -257,7 +261,7 @@ userSchema.methods.isCorrectPassword = async function ( return result; }; -userSchema.methods.passwordChanged = function (tokenIssuedAt: number): boolean { +userSchema.methods.passwordChanged = function(tokenIssuedAt: number): boolean { if ( this.changedPasswordAt && this.changedPasswordAt.getTime() / 1000 > tokenIssuedAt @@ -266,7 +270,7 @@ userSchema.methods.passwordChanged = function (tokenIssuedAt: number): boolean { return false; }; -userSchema.methods.generateSaveConfirmationCode = function (): string { +userSchema.methods.generateSaveConfirmationCode = function(): string { const confirmationCode: string = generateConfirmationCode(); this.emailVerificationCode = crypto .createHash('sha256') @@ -277,7 +281,7 @@ userSchema.methods.generateSaveConfirmationCode = function (): string { return confirmationCode; }; -userSchema.methods.createResetPasswordToken = function (): string { +userSchema.methods.createResetPasswordToken = function(): string { const resetPasswordToken = crypto.randomBytes(32).toString('hex'); this.resetPasswordToken = crypto diff --git a/src/routes/userRoute.ts b/src/routes/userRoute.ts index 8efc856..34753de 100644 --- a/src/routes/userRoute.ts +++ b/src/routes/userRoute.ts @@ -22,7 +22,8 @@ import { getAllGroups, activateUser, deactivateUser, - banUser + banUser, + updateFCMToken, } from '@controllers/userController'; import { deleteStory, @@ -31,7 +32,7 @@ import { getStory, postStory, } from '@controllers/storyController'; -import { protect,isAdmin,isActive } from '@middlewares/authMiddleware'; +import { protect, isAdmin, isActive } from '@middlewares/authMiddleware'; const router = Router(); @@ -48,10 +49,10 @@ router.post('/block/:id', block); router.delete('/block/:id', unblock); // Admin routes -router.patch('/activate/:userId',isAdmin,activateUser); -router.patch('/deactivate/:userId',isAdmin,deactivateUser); -router.patch('/ban/:userId',isAdmin,banUser); -router.get('/all-groups',isAdmin,getAllGroups); +router.patch('/activate/:userId', isAdmin, activateUser); +router.patch('/deactivate/:userId', isAdmin, deactivateUser); +router.patch('/ban/:userId', isAdmin, banUser); +router.get('/all-groups', isAdmin, getAllGroups); // User routes router.get('/me', getCurrentUser); @@ -63,11 +64,11 @@ router.patch('/email', updateEmail); router.patch('/username', updateUsername); router.patch('/screen-name', updateScreenName); router.patch('/picture', upload.single('file'), updatePicture); +router.patch('/fcm-token', updateFCMToken); router.delete('/picture', deletePicture); router.get('/contacts/stories', getAllContactsStories); router.get('/:userId/stories', getStory); router.get('/:userId', getUser); router.get('/', getAllUsers); - export default router; diff --git a/src/types/user.ts b/src/types/user.ts index 970d709..fd89b08 100644 --- a/src/types/user.ts +++ b/src/types/user.ts @@ -7,6 +7,7 @@ interface IUser extends Document { screenFirstName: string; screenLastName: string; email: string; + fcmToken: string; phoneNumber: string | undefined; password: string | undefined; passwordConfirm: string | undefined; From c61a87df79616bfd50ea47e384dc93fbdc70ea70 Mon Sep 17 00:00:00 2001 From: akramhany Date: Sat, 21 Dec 2024 22:03:31 +0200 Subject: [PATCH 53/54] feat(messages-notifications): implement normal messages notifications --- src/config/firebase.ts | 2 ++ src/sockets/messages.ts | 3 ++ src/sockets/notifications.ts | 56 ++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+) create mode 100644 src/sockets/notifications.ts diff --git a/src/config/firebase.ts b/src/config/firebase.ts index a47780d..6350540 100644 --- a/src/config/firebase.ts +++ b/src/config/firebase.ts @@ -8,4 +8,6 @@ admin.initializeApp({ credential: admin.credential.cert(serviceAccount), }); +export const messaging = admin.messaging(); + export default admin; diff --git a/src/sockets/messages.ts b/src/sockets/messages.ts index 9ace9fb..bab859d 100644 --- a/src/sockets/messages.ts +++ b/src/sockets/messages.ts @@ -5,6 +5,7 @@ import Message from '@models/messageModel'; import { enableDestruction } from '@services/chatService'; import Chat from '@base/models/chatModel'; import { check, informSessions, updateDraft } from './MessagingServices'; +import handleNotifications from './notifications'; interface PinUnPinMessageData { chatId: string | Types.ObjectId; @@ -82,6 +83,8 @@ const handleMessaging = async ( await message.save(); + handleNotifications(message.id.toString()); + if (parentMessage && isReply && chatType === 'channel') { parentMessage.threadMessages.push(message._id as Types.ObjectId); await parentMessage.save(); diff --git a/src/sockets/notifications.ts b/src/sockets/notifications.ts new file mode 100644 index 0000000..fdd4bcc --- /dev/null +++ b/src/sockets/notifications.ts @@ -0,0 +1,56 @@ +import { messaging } from '@base/config/firebase'; +import Chat from '@base/models/chatModel'; +import Message from '@base/models/messageModel'; +import User from '@base/models/userModel'; + +const sendNotification = async (fcmToken: string, title: string, body: any) => { + const message = { + notification: { + title, + body, + }, + token: fcmToken, + }; + + try { + const response = await messaging.send(message); + console.log('Notification sent successfully:', response); + } catch (error) { + console.error('Error sending notification:', error); + } +}; + +const sendNotificationToChat = async (senderId: string, chatId: string) => { + const targetChat = await Chat.findById(chatId).populate('members'); + + if (!targetChat) return; + + const memberIds = targetChat.members.filter( + (memberId) => memberId.toString() !== senderId + ); + + const members = await User.find({ _id: { $in: memberIds } }, 'chats'); + + members.forEach((member) => { + const targetChatInfo = member.chats.find( + ({ chat, isMuted }) => chat.toString() === chatId.toString() && !isMuted + ); + + if (targetChatInfo) { + sendNotification( + member.fcmToken, + 'Message Received', + `Message received from ${member.username}` + ); + } + }); +}; + +const handleNotifications = async (messageId: string) => { + const message = await Message.findById(messageId); + + const { senderId, chatId } = message; + sendNotificationToChat(senderId, chatId); +}; + +export default handleNotifications; From a4867b67c306ca1214838e0e1c248d7ad19bffa3 Mon Sep 17 00:00:00 2001 From: AhmedHamed3699 Date: Sat, 21 Dec 2024 22:35:23 +0200 Subject: [PATCH 54/54] fix: changed mention regex --- src/services/chatService.ts | 31 +++++++++++++++++-------------- src/sockets/messages.ts | 9 +++++---- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/src/services/chatService.ts b/src/services/chatService.ts index f3405f7..a220abe 100644 --- a/src/services/chatService.ts +++ b/src/services/chatService.ts @@ -25,23 +25,26 @@ export const getLastMessage = async (chats: any) => { return lastMessages; }; -export const getUnreadMessages = async (chats: any, user: any) => - Promise.all( - chats.map(async (chat: any) => ({ - chatId: chat.chat._id, - unreadMessagesCount: await Message.countDocuments({ +export const getUnreadMessages = async (chats: any, user: any) => { + const mentionRegex = /@[[^]]+](([^)]+))/g; + return Promise.all( + chats.map(async (chat: any) => { + const unreadMessages = await Message.find({ chatId: chat.chat._id, + senderId: { $ne: user._id }, readBy: { $nin: [user._id] }, - }), - isMentioned: - (await Message.exists({ - chatId: chat.chat._id, - readBy: { $nin: [user._id] }, - senderId: { $ne: user._id }, - content: new RegExp(`@${user.username}`, 'i'), - })) !== null, - })) + }); + return { + chatId: chat.chat._id, + unreadMessagesCount: unreadMessages.length, + isMentioned: + unreadMessages.filter((message: any) => + mentionRegex.test(message.content) + ).length > 0, + }; + }) ); +}; export const getChats = async ( userId: mongoose.Types.ObjectId, diff --git a/src/sockets/messages.ts b/src/sockets/messages.ts index bab859d..dce8126 100644 --- a/src/sockets/messages.ts +++ b/src/sockets/messages.ts @@ -107,11 +107,12 @@ const handleMessaging = async ( ); } }); - const res = { - messageId: message._id, - }; enableDestruction(socket, message, chatId); - ack({ success: true, message: 'Message sent successfully', res }); + ack({ + success: true, + message: 'Message sent successfully', + data: message, + }); }; const handleEditMessage = async (socket: Socket, data: any, ack: Function) => {