@enfin/chat-server 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +278 -0
- package/dist/bin/start.js +91 -0
- package/dist/chat-mongoose.module.js +48 -0
- package/dist/chat.constants.js +5 -0
- package/dist/chat.module.js +144 -0
- package/dist/controllers/rooms.controller.js +112 -0
- package/dist/controllers/upload.controller.js +123 -0
- package/dist/controllers/users.controller.js +101 -0
- package/dist/controllers/validation.controller.js +44 -0
- package/dist/dto/call.dto.js +21 -0
- package/dist/dto/message.dto.js +58 -0
- package/dist/dto/presence.dto.js +20 -0
- package/dist/dto/room.dto.js +48 -0
- package/dist/gateways/chat.gateway.js +439 -0
- package/dist/index.js +26 -0
- package/dist/interfaces/index.js +2 -0
- package/dist/main.js +38 -0
- package/dist/schemas/apikey.schema.js +33 -0
- package/dist/schemas/audiocall.schema.js +49 -0
- package/dist/schemas/index.js +34 -0
- package/dist/schemas/message.schema.js +40 -0
- package/dist/schemas/presence.schema.js +23 -0
- package/dist/schemas/room.schema.js +40 -0
- package/dist/schemas/user.schema.js +14 -0
- package/dist/services/apikey.service.js +120 -0
- package/dist/services/call.service.js +145 -0
- package/dist/services/message.service.js +108 -0
- package/dist/services/presence.service.js +65 -0
- package/dist/services/room.service.js +158 -0
- package/dist/services/storage/rate-limiter.service.js +57 -0
- package/dist/services/storage/storage.service.js +96 -0
- package/dist/services/storage/upload-handler.interface.js +2 -0
- package/package.json +68 -0
|
@@ -0,0 +1,439 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
var ChatGateway_1;
|
|
15
|
+
var _a, _b, _c, _d;
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.ChatGateway = void 0;
|
|
18
|
+
const websockets_1 = require("@nestjs/websockets");
|
|
19
|
+
const socket_io_1 = require("socket.io");
|
|
20
|
+
const common_1 = require("@nestjs/common");
|
|
21
|
+
const message_service_1 = require("../services/message.service");
|
|
22
|
+
const room_service_1 = require("../services/room.service");
|
|
23
|
+
const presence_service_1 = require("../services/presence.service");
|
|
24
|
+
const call_service_1 = require("../services/call.service");
|
|
25
|
+
const apikey_service_1 = require("../services/apikey.service");
|
|
26
|
+
const chat_shared_1 = require("../../../shared/dist/index.js");
|
|
27
|
+
let ChatGateway = ChatGateway_1 = class ChatGateway {
|
|
28
|
+
constructor(messageService, roomService, presenceService, callService, apiKeyService) {
|
|
29
|
+
this.messageService = messageService;
|
|
30
|
+
this.roomService = roomService;
|
|
31
|
+
this.presenceService = presenceService;
|
|
32
|
+
this.callService = callService;
|
|
33
|
+
this.apiKeyService = apiKeyService;
|
|
34
|
+
this.logger = new common_1.Logger(ChatGateway_1.name);
|
|
35
|
+
this.connectedUsers = new Map();
|
|
36
|
+
}
|
|
37
|
+
afterInit(server) {
|
|
38
|
+
this.logger.log('========================================');
|
|
39
|
+
this.logger.log('[GATEWAY] WebSocket Gateway initialized on namespace /chat');
|
|
40
|
+
this.logger.log('========================================');
|
|
41
|
+
}
|
|
42
|
+
async handleConnection(client) {
|
|
43
|
+
const auth = client.handshake.auth;
|
|
44
|
+
console.log('========================================');
|
|
45
|
+
console.log('[GATEWAY] handleConnection — socket.id =', client.id);
|
|
46
|
+
console.log('[GATEWAY] handshake.auth =', JSON.stringify(auth));
|
|
47
|
+
console.log('========================================');
|
|
48
|
+
const { apiKey, userId, userName } = auth;
|
|
49
|
+
if (!apiKey) {
|
|
50
|
+
console.log('[GATEWAY] Missing apiKey in handshake — disconnecting client', client.id);
|
|
51
|
+
client.emit('error', { code: 'MISSING_API_KEY', message: 'apiKey is required in handshake auth' });
|
|
52
|
+
client.disconnect();
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (!userId || !userName) {
|
|
56
|
+
console.log('[GATEWAY] Missing userId/userName — disconnecting client', client.id);
|
|
57
|
+
client.emit('error', { code: 'MISSING_USER', message: 'userId and userName are required' });
|
|
58
|
+
client.disconnect();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const validation = await this.apiKeyService.validateKey(apiKey);
|
|
62
|
+
if (!validation.valid) {
|
|
63
|
+
console.log(`[GATEWAY] Invalid apiKey=${apiKey.slice(0, 12)}... reason=${validation.error} — disconnecting`);
|
|
64
|
+
client.emit('error', { code: 'INVALID_API_KEY', message: validation.error || 'Invalid apiKey' });
|
|
65
|
+
client.disconnect();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const tenantId = validation.tenantId;
|
|
69
|
+
const mode = validation.mode;
|
|
70
|
+
this.connectedUsers.set(client.id, { userId, userName, tenantId, apiKey, mode });
|
|
71
|
+
await this.presenceService.setOnline(tenantId, userId);
|
|
72
|
+
// Broadcast presence change to all other connected clients
|
|
73
|
+
this.server.emit('presence:changed', { userId, status: 'online' });
|
|
74
|
+
console.log(`[GATEWAY] User connected: userId=${userId} userName=${userName} tenantId=${tenantId} mode=${mode} socketId=${client.id}`);
|
|
75
|
+
console.log(`[GATEWAY] Total connected users:`, this.connectedUsers.size);
|
|
76
|
+
}
|
|
77
|
+
async handleDisconnect(client) {
|
|
78
|
+
const user = this.connectedUsers.get(client.id);
|
|
79
|
+
if (user) {
|
|
80
|
+
// Check if user has other active connections
|
|
81
|
+
const otherConnections = Array.from(this.connectedUsers.values()).filter((u, idx) => u.userId === user.userId && idx !== this._keyIndex(client.id));
|
|
82
|
+
// Only mark offline if no other connections exist for this user
|
|
83
|
+
if (otherConnections.length === 0) {
|
|
84
|
+
await this.presenceService.setOffline(user.tenantId, user.userId);
|
|
85
|
+
// Broadcast presence change to all other connected clients
|
|
86
|
+
this.server.emit('presence:changed', { userId: user.userId, status: 'offline' });
|
|
87
|
+
// End any ringing/active calls this user was in. Refresh, network loss,
|
|
88
|
+
// or browser crash otherwise leaves the call record stuck and blocks
|
|
89
|
+
// subsequent call attempts.
|
|
90
|
+
try {
|
|
91
|
+
const endedCalls = await this.callService.endCallsForUser(user.tenantId, user.userId, 'peer_disconnected');
|
|
92
|
+
for (const call of endedCalls) {
|
|
93
|
+
console.log(`[GATEWAY] Auto-ended call ${call._id} in room ${call.roomId} due to disconnect of userId=${user.userId}`);
|
|
94
|
+
const updated = await this.callService.getCall(user.tenantId, call._id.toString());
|
|
95
|
+
this.emitToRoom(call.roomId, 'call:ended', updated);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (err) {
|
|
99
|
+
console.log(`[GATEWAY] Failed to end calls on disconnect for user=${user.userId}:`, err?.message || err);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
this.connectedUsers.delete(client.id);
|
|
103
|
+
console.log(`[GATEWAY] User disconnected: userId=${user.userId} socketId=${client.id}`);
|
|
104
|
+
console.log(`[GATEWAY] Total connected users remaining:`, this.connectedUsers.size);
|
|
105
|
+
}
|
|
106
|
+
else {
|
|
107
|
+
console.log(`[GATEWAY] Unknown socket disconnected: ${client.id}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
_keyIndex(key) {
|
|
111
|
+
let i = 0;
|
|
112
|
+
for (const k of this.connectedUsers.keys()) {
|
|
113
|
+
if (k === key)
|
|
114
|
+
return i;
|
|
115
|
+
i++;
|
|
116
|
+
}
|
|
117
|
+
return -1;
|
|
118
|
+
}
|
|
119
|
+
async handleMessage(client, payload) {
|
|
120
|
+
const user = this.connectedUsers.get(client.id);
|
|
121
|
+
if (!user)
|
|
122
|
+
return;
|
|
123
|
+
console.log('Received message payload from user:', user.userId, payload);
|
|
124
|
+
console.log(`[GATEWAY] message:send — user=${user.userId} payload=${JSON.stringify(payload)}`);
|
|
125
|
+
this.logger.log(`Received message: user=${user.userId} payload=${JSON.stringify(payload)}`);
|
|
126
|
+
console.log('roomId :', payload?.roomId);
|
|
127
|
+
console.log('!payload :', !payload);
|
|
128
|
+
if (!payload || !payload.roomId) {
|
|
129
|
+
const stored = this.connectedUsers.get(client.id);
|
|
130
|
+
const fallbackRoom = stored?.activeRoomId;
|
|
131
|
+
if (fallbackRoom) {
|
|
132
|
+
payload = { ...(payload || {}), roomId: fallbackRoom };
|
|
133
|
+
this.logger.log(`Using fallback roomId=${fallbackRoom} for user=${user.userId}`);
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
this.logger.warn(`Invalid message payload from user=${user.userId}: missing roomId`);
|
|
137
|
+
throw new websockets_1.WsException('roomId is required');
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
console.log('[GATEWAY] Calling messageService.createMessage with payload:', JSON.stringify(payload));
|
|
141
|
+
let message;
|
|
142
|
+
try {
|
|
143
|
+
message = await this.messageService.createMessage(user.tenantId, payload, user.userId, user.userName);
|
|
144
|
+
}
|
|
145
|
+
catch (err) {
|
|
146
|
+
console.log('[GATEWAY] ERROR in createMessage:', err?.message || err);
|
|
147
|
+
console.log('[GATEWAY] Stack:', err?.stack);
|
|
148
|
+
console.log('[GATEWAY] Validation errors:', JSON.stringify(err?.errors || {}));
|
|
149
|
+
throw err;
|
|
150
|
+
}
|
|
151
|
+
console.log('[GATEWAY] messageService.createMessage returned:');
|
|
152
|
+
console.log('[GATEWAY] _id =', message?._id);
|
|
153
|
+
console.log('[GATEWAY] roomId =', message?.roomId);
|
|
154
|
+
console.log('[GATEWAY] senderId =', message?.senderId);
|
|
155
|
+
console.log('[GATEWAY] content =', message?.content);
|
|
156
|
+
console.log('[GATEWAY] toJSON =', JSON.stringify(message));
|
|
157
|
+
console.log('Gateway created message:', message?._id, 'for room:', payload.roomId);
|
|
158
|
+
// Get room members and emit to them
|
|
159
|
+
const room = await this.roomService.getRoom(user.tenantId, payload.roomId);
|
|
160
|
+
const roomChannel = `room:${payload.roomId}`;
|
|
161
|
+
console.log(`[GATEWAY] Room channel =`, roomChannel);
|
|
162
|
+
// Auto-join all connected sockets belonging to room members to the room channel
|
|
163
|
+
// This ensures every member receives messages even if they haven't explicitly joined
|
|
164
|
+
if (room && Array.isArray(room.members)) {
|
|
165
|
+
const memberIds = new Set(room.members);
|
|
166
|
+
for (const [socketId, socketUser] of this.connectedUsers.entries()) {
|
|
167
|
+
if (memberIds.has(socketUser.userId)) {
|
|
168
|
+
const sock = this.server.sockets.get(socketId);
|
|
169
|
+
if (sock) {
|
|
170
|
+
try {
|
|
171
|
+
await sock.join(roomChannel);
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
console.log('[GATEWAY] Auto-join error for socket', socketId, e?.message);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
else {
|
|
181
|
+
// No room found or no members — at least ensure sender is in channel
|
|
182
|
+
client.join(roomChannel);
|
|
183
|
+
}
|
|
184
|
+
const socketsInRoom = await this.server.in(roomChannel).fetchSockets();
|
|
185
|
+
console.log(`[GATEWAY] Sockets in room ${roomChannel}: ${socketsInRoom.length} sockets`);
|
|
186
|
+
console.log(`[GATEWAY] Socket IDs:`, socketsInRoom.map(s => s.id));
|
|
187
|
+
// Broadcast to all sockets in the room
|
|
188
|
+
console.log(`[GATEWAY] -> emitToRoom(${payload.roomId}, 'message:new', message)`);
|
|
189
|
+
this.emitToRoom(payload.roomId, 'message:new', message);
|
|
190
|
+
return message;
|
|
191
|
+
}
|
|
192
|
+
async handleTyping(client, payload) {
|
|
193
|
+
const user = this.connectedUsers.get(client.id);
|
|
194
|
+
if (!user)
|
|
195
|
+
return;
|
|
196
|
+
const p = payload;
|
|
197
|
+
const isTyping = !!(p.typing || p.content);
|
|
198
|
+
if (isTyping) {
|
|
199
|
+
await this.presenceService.setTyping(user.tenantId, user.userId, p.roomId);
|
|
200
|
+
}
|
|
201
|
+
else {
|
|
202
|
+
await this.presenceService.stopTyping(user.tenantId, user.userId, p.roomId);
|
|
203
|
+
}
|
|
204
|
+
// Broadcast typing to room members
|
|
205
|
+
this.emitToRoomExcept(p.roomId, 'message:typing', { userId: user.userId, userName: user.userName, roomId: p.roomId, typing: isTyping }, user.userId);
|
|
206
|
+
}
|
|
207
|
+
async handleRoomSelect(client, payload) {
|
|
208
|
+
console.log('========================================');
|
|
209
|
+
console.log('[GATEWAY] room:select received');
|
|
210
|
+
console.log('[GATEWAY] client.id =', client.id);
|
|
211
|
+
console.log('[GATEWAY] raw payload =', JSON.stringify(payload));
|
|
212
|
+
console.log('========================================');
|
|
213
|
+
const user = this.connectedUsers.get(client.id);
|
|
214
|
+
if (!user) {
|
|
215
|
+
console.log('[GATEWAY] room:select — unknown client, returning');
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
const { roomId } = payload || {};
|
|
219
|
+
if (!roomId) {
|
|
220
|
+
console.log('[GATEWAY] room:select — missing roomId, returning');
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
console.log(`[GATEWAY] room:select — user=${user.userId} joining room:${roomId}`);
|
|
224
|
+
// Track active room on connected user and join socket to room channel
|
|
225
|
+
const existing = this.connectedUsers.get(client.id) || { userId: user.userId, userName: user.userName, tenantId: user.tenantId };
|
|
226
|
+
this.connectedUsers.set(client.id, { ...existing, activeRoomId: roomId });
|
|
227
|
+
try {
|
|
228
|
+
client.join(`room:${roomId}`);
|
|
229
|
+
this.logger.log(`Socket ${client.id} joined room:${roomId} for user=${user.userId}`);
|
|
230
|
+
}
|
|
231
|
+
catch (err) {
|
|
232
|
+
this.logger.warn(`Failed to join socket to room:${roomId}: ${err}`);
|
|
233
|
+
}
|
|
234
|
+
return { ok: true };
|
|
235
|
+
}
|
|
236
|
+
async handleRoomJoin(client, payload) {
|
|
237
|
+
const user = this.connectedUsers.get(client.id);
|
|
238
|
+
if (!user) {
|
|
239
|
+
return { ok: false, error: 'unknown client' };
|
|
240
|
+
}
|
|
241
|
+
const { roomId } = payload || {};
|
|
242
|
+
if (!roomId) {
|
|
243
|
+
return { ok: false, error: 'roomId required' };
|
|
244
|
+
}
|
|
245
|
+
try {
|
|
246
|
+
// Verify the user is a member of this room
|
|
247
|
+
const isMember = await this.roomService.isMember(user.tenantId, roomId, user.userId);
|
|
248
|
+
if (!isMember) {
|
|
249
|
+
console.log(`[GATEWAY] room:join — user=${user.userId} is NOT a member of room=${roomId}`);
|
|
250
|
+
return { ok: false, error: 'not a member' };
|
|
251
|
+
}
|
|
252
|
+
client.join(`room:${roomId}`);
|
|
253
|
+
console.log(`[GATEWAY] room:join — user=${user.userId} joined room:${roomId}`);
|
|
254
|
+
return { ok: true, roomId };
|
|
255
|
+
}
|
|
256
|
+
catch (err) {
|
|
257
|
+
console.log(`[GATEWAY] room:join — error:`, err?.message || err);
|
|
258
|
+
return { ok: false, error: err?.message || 'join failed' };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
async handlePresenceUpdate(client, payload) {
|
|
262
|
+
const user = this.connectedUsers.get(client.id);
|
|
263
|
+
if (!user)
|
|
264
|
+
return;
|
|
265
|
+
await this.presenceService.setStatus(user.tenantId, user.userId, payload);
|
|
266
|
+
// Broadcast presence to all connected users
|
|
267
|
+
this.server.emit('presence:changed', {
|
|
268
|
+
userId: user.userId,
|
|
269
|
+
status: payload.status,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
async handleCallInitiate(client, payload) {
|
|
273
|
+
const user = this.connectedUsers.get(client.id);
|
|
274
|
+
if (!user)
|
|
275
|
+
return;
|
|
276
|
+
const call = await this.callService.initiateCall(user.tenantId, payload.roomId, user.userId, payload.participantId);
|
|
277
|
+
// Notify the participant
|
|
278
|
+
this.emitToUser(payload.participantId, 'call:incoming', call);
|
|
279
|
+
return call;
|
|
280
|
+
}
|
|
281
|
+
async handleCallAccept(client, payload) {
|
|
282
|
+
const user = this.connectedUsers.get(client.id);
|
|
283
|
+
if (!user)
|
|
284
|
+
return;
|
|
285
|
+
const call = await this.callService.acceptCall(user.tenantId, payload.callId, user.userId);
|
|
286
|
+
// Notify both parties
|
|
287
|
+
this.emitToRoom(call.roomId, 'call:accepted', call);
|
|
288
|
+
return call;
|
|
289
|
+
}
|
|
290
|
+
async handleCallReject(client, payload) {
|
|
291
|
+
const user = this.connectedUsers.get(client.id);
|
|
292
|
+
if (!user)
|
|
293
|
+
return;
|
|
294
|
+
const call = await this.callService.rejectCall(user.tenantId, payload.callId, user.userId);
|
|
295
|
+
this.emitToRoom(call.roomId, 'call:rejected', call);
|
|
296
|
+
return call;
|
|
297
|
+
}
|
|
298
|
+
async handleCallEnd(client, payload) {
|
|
299
|
+
const user = this.connectedUsers.get(client.id);
|
|
300
|
+
if (!user)
|
|
301
|
+
return;
|
|
302
|
+
const call = await this.callService.endCall(user.tenantId, payload.callId);
|
|
303
|
+
this.emitToRoom(call.roomId, 'call:ended', call);
|
|
304
|
+
return call;
|
|
305
|
+
}
|
|
306
|
+
async handleCallSignal(client, payload) {
|
|
307
|
+
const user = this.connectedUsers.get(client.id);
|
|
308
|
+
if (!user)
|
|
309
|
+
return;
|
|
310
|
+
const call = await this.callService.getActiveCall(user.tenantId, payload.roomId);
|
|
311
|
+
console.log();
|
|
312
|
+
if (!call)
|
|
313
|
+
return;
|
|
314
|
+
const targetId = call.initiatorId === user.userId ? call.participantId : call.initiatorId;
|
|
315
|
+
this.emitToUser(targetId, 'call:signal', payload);
|
|
316
|
+
}
|
|
317
|
+
emitToRoom(roomId, event, data, excludeUserId) {
|
|
318
|
+
console.log(`[GATEWAY] emitToRoom(roomId=${roomId}, event='${event}', excludeUserId=${excludeUserId})`);
|
|
319
|
+
this.server.to(`room:${roomId}`).emit(event, data);
|
|
320
|
+
}
|
|
321
|
+
emitToRoomExcept(roomId, event, data, excludeUserId) {
|
|
322
|
+
console.log(`[GATEWAY] emitToRoomExcept(roomId=${roomId}, event='${event}', excludeUserId=${excludeUserId})`);
|
|
323
|
+
const roomChannel = `room:${roomId}`;
|
|
324
|
+
for (const [socketId, user] of this.connectedUsers.entries()) {
|
|
325
|
+
if (user.userId === excludeUserId)
|
|
326
|
+
continue;
|
|
327
|
+
const sock = this.server.sockets.get(socketId);
|
|
328
|
+
if (sock && sock.rooms?.has(roomChannel)) {
|
|
329
|
+
sock.emit(event, data);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
emitToUser(userId, event, data) {
|
|
334
|
+
console.log(`[GATEWAY] emitToUser(userId=${userId}, event='${event}')`);
|
|
335
|
+
for (const [socketId, user] of this.connectedUsers.entries()) {
|
|
336
|
+
if (user.userId === userId) {
|
|
337
|
+
this.server.to(socketId).emit(event, data);
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
};
|
|
342
|
+
exports.ChatGateway = ChatGateway;
|
|
343
|
+
__decorate([
|
|
344
|
+
(0, websockets_1.WebSocketServer)(),
|
|
345
|
+
__metadata("design:type", socket_io_1.Server)
|
|
346
|
+
], ChatGateway.prototype, "server", void 0);
|
|
347
|
+
__decorate([
|
|
348
|
+
(0, websockets_1.SubscribeMessage)('message:send'),
|
|
349
|
+
__param(0, (0, websockets_1.ConnectedSocket)()),
|
|
350
|
+
__param(1, (0, websockets_1.MessageBody)()),
|
|
351
|
+
__metadata("design:type", Function),
|
|
352
|
+
__metadata("design:paramtypes", [socket_io_1.Socket, typeof (_a = typeof chat_shared_1.SendMessagePayload !== "undefined" && chat_shared_1.SendMessagePayload) === "function" ? _a : Object]),
|
|
353
|
+
__metadata("design:returntype", Promise)
|
|
354
|
+
], ChatGateway.prototype, "handleMessage", null);
|
|
355
|
+
__decorate([
|
|
356
|
+
(0, websockets_1.SubscribeMessage)('message:typing'),
|
|
357
|
+
__param(0, (0, websockets_1.ConnectedSocket)()),
|
|
358
|
+
__param(1, (0, websockets_1.MessageBody)()),
|
|
359
|
+
__metadata("design:type", Function),
|
|
360
|
+
__metadata("design:paramtypes", [socket_io_1.Socket, typeof (_b = typeof chat_shared_1.TypingPayload !== "undefined" && chat_shared_1.TypingPayload) === "function" ? _b : Object]),
|
|
361
|
+
__metadata("design:returntype", Promise)
|
|
362
|
+
], ChatGateway.prototype, "handleTyping", null);
|
|
363
|
+
__decorate([
|
|
364
|
+
(0, websockets_1.SubscribeMessage)('room:select'),
|
|
365
|
+
__param(0, (0, websockets_1.ConnectedSocket)()),
|
|
366
|
+
__param(1, (0, websockets_1.MessageBody)()),
|
|
367
|
+
__metadata("design:type", Function),
|
|
368
|
+
__metadata("design:paramtypes", [socket_io_1.Socket, Object]),
|
|
369
|
+
__metadata("design:returntype", Promise)
|
|
370
|
+
], ChatGateway.prototype, "handleRoomSelect", null);
|
|
371
|
+
__decorate([
|
|
372
|
+
(0, websockets_1.SubscribeMessage)('room:join'),
|
|
373
|
+
__param(0, (0, websockets_1.ConnectedSocket)()),
|
|
374
|
+
__param(1, (0, websockets_1.MessageBody)()),
|
|
375
|
+
__metadata("design:type", Function),
|
|
376
|
+
__metadata("design:paramtypes", [socket_io_1.Socket, Object]),
|
|
377
|
+
__metadata("design:returntype", Promise)
|
|
378
|
+
], ChatGateway.prototype, "handleRoomJoin", null);
|
|
379
|
+
__decorate([
|
|
380
|
+
(0, websockets_1.SubscribeMessage)('presence:update'),
|
|
381
|
+
__param(0, (0, websockets_1.ConnectedSocket)()),
|
|
382
|
+
__param(1, (0, websockets_1.MessageBody)()),
|
|
383
|
+
__metadata("design:type", Function),
|
|
384
|
+
__metadata("design:paramtypes", [socket_io_1.Socket, typeof (_c = typeof chat_shared_1.PresencePayload !== "undefined" && chat_shared_1.PresencePayload) === "function" ? _c : Object]),
|
|
385
|
+
__metadata("design:returntype", Promise)
|
|
386
|
+
], ChatGateway.prototype, "handlePresenceUpdate", null);
|
|
387
|
+
__decorate([
|
|
388
|
+
(0, websockets_1.SubscribeMessage)('call:initiate'),
|
|
389
|
+
__param(0, (0, websockets_1.ConnectedSocket)()),
|
|
390
|
+
__param(1, (0, websockets_1.MessageBody)()),
|
|
391
|
+
__metadata("design:type", Function),
|
|
392
|
+
__metadata("design:paramtypes", [socket_io_1.Socket, Object]),
|
|
393
|
+
__metadata("design:returntype", Promise)
|
|
394
|
+
], ChatGateway.prototype, "handleCallInitiate", null);
|
|
395
|
+
__decorate([
|
|
396
|
+
(0, websockets_1.SubscribeMessage)('call:accept'),
|
|
397
|
+
__param(0, (0, websockets_1.ConnectedSocket)()),
|
|
398
|
+
__param(1, (0, websockets_1.MessageBody)()),
|
|
399
|
+
__metadata("design:type", Function),
|
|
400
|
+
__metadata("design:paramtypes", [socket_io_1.Socket, Object]),
|
|
401
|
+
__metadata("design:returntype", Promise)
|
|
402
|
+
], ChatGateway.prototype, "handleCallAccept", null);
|
|
403
|
+
__decorate([
|
|
404
|
+
(0, websockets_1.SubscribeMessage)('call:reject'),
|
|
405
|
+
__param(0, (0, websockets_1.ConnectedSocket)()),
|
|
406
|
+
__param(1, (0, websockets_1.MessageBody)()),
|
|
407
|
+
__metadata("design:type", Function),
|
|
408
|
+
__metadata("design:paramtypes", [socket_io_1.Socket, Object]),
|
|
409
|
+
__metadata("design:returntype", Promise)
|
|
410
|
+
], ChatGateway.prototype, "handleCallReject", null);
|
|
411
|
+
__decorate([
|
|
412
|
+
(0, websockets_1.SubscribeMessage)('call:end'),
|
|
413
|
+
__param(0, (0, websockets_1.ConnectedSocket)()),
|
|
414
|
+
__param(1, (0, websockets_1.MessageBody)()),
|
|
415
|
+
__metadata("design:type", Function),
|
|
416
|
+
__metadata("design:paramtypes", [socket_io_1.Socket, Object]),
|
|
417
|
+
__metadata("design:returntype", Promise)
|
|
418
|
+
], ChatGateway.prototype, "handleCallEnd", null);
|
|
419
|
+
__decorate([
|
|
420
|
+
(0, websockets_1.SubscribeMessage)('call:signal'),
|
|
421
|
+
__param(0, (0, websockets_1.ConnectedSocket)()),
|
|
422
|
+
__param(1, (0, websockets_1.MessageBody)()),
|
|
423
|
+
__metadata("design:type", Function),
|
|
424
|
+
__metadata("design:paramtypes", [socket_io_1.Socket, typeof (_d = typeof chat_shared_1.CallSignalingPayload !== "undefined" && chat_shared_1.CallSignalingPayload) === "function" ? _d : Object]),
|
|
425
|
+
__metadata("design:returntype", Promise)
|
|
426
|
+
], ChatGateway.prototype, "handleCallSignal", null);
|
|
427
|
+
exports.ChatGateway = ChatGateway = ChatGateway_1 = __decorate([
|
|
428
|
+
(0, websockets_1.WebSocketGateway)({
|
|
429
|
+
cors: {
|
|
430
|
+
origin: '*',
|
|
431
|
+
},
|
|
432
|
+
namespace: '/chat',
|
|
433
|
+
}),
|
|
434
|
+
__metadata("design:paramtypes", [message_service_1.MessageService,
|
|
435
|
+
room_service_1.RoomService,
|
|
436
|
+
presence_service_1.PresenceService,
|
|
437
|
+
call_service_1.CallService,
|
|
438
|
+
apikey_service_1.ApiKeyService])
|
|
439
|
+
], ChatGateway);
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.ChatGateway = void 0;
|
|
18
|
+
__exportStar(require("./chat.module"), exports);
|
|
19
|
+
__exportStar(require("./chat-mongoose.module"), exports);
|
|
20
|
+
__exportStar(require("./schemas"), exports);
|
|
21
|
+
__exportStar(require("./services/message.service"), exports);
|
|
22
|
+
__exportStar(require("./services/room.service"), exports);
|
|
23
|
+
__exportStar(require("./services/presence.service"), exports);
|
|
24
|
+
__exportStar(require("./services/call.service"), exports);
|
|
25
|
+
var chat_gateway_1 = require("./gateways/chat.gateway");
|
|
26
|
+
Object.defineProperty(exports, "ChatGateway", { enumerable: true, get: function () { return chat_gateway_1.ChatGateway; } });
|
package/dist/main.js
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
/**
|
|
4
|
+
* Standalone entry point — NOT needed when ChatModule is mounted in developer's existing NestJS app.
|
|
5
|
+
*
|
|
6
|
+
* For standalone dev testing only:
|
|
7
|
+
* npm run start
|
|
8
|
+
* Or:
|
|
9
|
+
* PORT=3002 node dist/main.js
|
|
10
|
+
*
|
|
11
|
+
* For production / developer install — they mount ChatModule.forRoot in their own AppModule.
|
|
12
|
+
*/
|
|
13
|
+
require("reflect-metadata");
|
|
14
|
+
const core_1 = require("@nestjs/core");
|
|
15
|
+
const common_1 = require("@nestjs/common");
|
|
16
|
+
const chat_module_1 = require("./chat.module");
|
|
17
|
+
async function bootstrap() {
|
|
18
|
+
const app = await core_1.NestFactory.create(chat_module_1.ChatModule.forRoot({
|
|
19
|
+
apiKey: process.env.CHAT_API_KEY || 'chat_dev_key',
|
|
20
|
+
mongoUri: process.env.MONGO_URI,
|
|
21
|
+
mode: process.env.CHAT_MODE || 'managed',
|
|
22
|
+
version: process.env.CHAT_VERSION || '1.0.0',
|
|
23
|
+
}));
|
|
24
|
+
app.enableCors({
|
|
25
|
+
origin: '*',
|
|
26
|
+
credentials: true,
|
|
27
|
+
});
|
|
28
|
+
app.useGlobalPipes(new common_1.ValidationPipe({
|
|
29
|
+
whitelist: true,
|
|
30
|
+
transform: true,
|
|
31
|
+
}));
|
|
32
|
+
app.setGlobalPrefix('api');
|
|
33
|
+
const port = process.env.PORT || 3002;
|
|
34
|
+
await app.listen(port);
|
|
35
|
+
console.log(`ChatModule (standalone) running on port ${port}`);
|
|
36
|
+
console.log(`Use ChatModule.forRoot in your own NestJS app instead of running this standalone.`);
|
|
37
|
+
}
|
|
38
|
+
bootstrap();
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ApiKey = exports.ApiKeySchema = void 0;
|
|
4
|
+
const mongoose_1 = require("mongoose");
|
|
5
|
+
exports.ApiKeySchema = new mongoose_1.Schema({
|
|
6
|
+
apiKey: { type: String, required: true, unique: true, index: true },
|
|
7
|
+
tenantId: { type: String, required: true, index: true },
|
|
8
|
+
mode: {
|
|
9
|
+
type: String,
|
|
10
|
+
enum: ['managed', 'external-db'],
|
|
11
|
+
required: true,
|
|
12
|
+
default: 'managed',
|
|
13
|
+
},
|
|
14
|
+
database: { type: String },
|
|
15
|
+
version: { type: String, required: true, default: '1.0.0' },
|
|
16
|
+
status: {
|
|
17
|
+
type: String,
|
|
18
|
+
enum: ['active', 'revoked', 'expired'],
|
|
19
|
+
required: true,
|
|
20
|
+
default: 'active',
|
|
21
|
+
},
|
|
22
|
+
}, {
|
|
23
|
+
timestamps: true,
|
|
24
|
+
toJSON: {
|
|
25
|
+
virtuals: true,
|
|
26
|
+
transform: function (doc, ret) {
|
|
27
|
+
ret.id = ret._id;
|
|
28
|
+
delete ret.__v;
|
|
29
|
+
return ret;
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
});
|
|
33
|
+
exports.ApiKey = (0, mongoose_1.model)('ApiKey', exports.ApiKeySchema);
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.AudioCall = exports.AudioCallSchema = void 0;
|
|
4
|
+
const mongoose_1 = require("mongoose");
|
|
5
|
+
exports.AudioCallSchema = new mongoose_1.Schema({
|
|
6
|
+
tenantId: { type: String, required: true, index: true },
|
|
7
|
+
roomId: {
|
|
8
|
+
type: String,
|
|
9
|
+
required: true,
|
|
10
|
+
index: true
|
|
11
|
+
},
|
|
12
|
+
initiatorId: {
|
|
13
|
+
type: String,
|
|
14
|
+
required: true,
|
|
15
|
+
index: true
|
|
16
|
+
},
|
|
17
|
+
participantId: {
|
|
18
|
+
type: String,
|
|
19
|
+
required: true,
|
|
20
|
+
index: true
|
|
21
|
+
},
|
|
22
|
+
status: {
|
|
23
|
+
type: String,
|
|
24
|
+
enum: ['ringing', 'active', 'ended'],
|
|
25
|
+
default: 'ringing'
|
|
26
|
+
},
|
|
27
|
+
startedAt: { type: Date, default: Date.now },
|
|
28
|
+
endedAt: { type: Date },
|
|
29
|
+
duration: { type: Number },
|
|
30
|
+
endReason: { type: String }
|
|
31
|
+
}, {
|
|
32
|
+
timestamps: false,
|
|
33
|
+
toJSON: {
|
|
34
|
+
virtuals: true,
|
|
35
|
+
transform: function (doc, ret) {
|
|
36
|
+
ret.id = ret._id;
|
|
37
|
+
delete ret.__v;
|
|
38
|
+
return ret;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
// Index for finding active calls
|
|
43
|
+
exports.AudioCallSchema.index({ tenantId: 1, status: 1 });
|
|
44
|
+
exports.AudioCallSchema.index({ tenantId: 1, roomId: 1, status: 1 });
|
|
45
|
+
// Indexes for the cross-call busy check (look up by initiatorId or participantId)
|
|
46
|
+
exports.AudioCallSchema.index({ tenantId: 1, initiatorId: 1, status: 1 });
|
|
47
|
+
exports.AudioCallSchema.index({ tenantId: 1, participantId: 1, status: 1 });
|
|
48
|
+
// Export model after schema is declared
|
|
49
|
+
exports.AudioCall = (0, mongoose_1.model)('AudioCall', exports.AudioCallSchema);
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.schemas = void 0;
|
|
18
|
+
__exportStar(require("./message.schema"), exports);
|
|
19
|
+
__exportStar(require("./room.schema"), exports);
|
|
20
|
+
__exportStar(require("./presence.schema"), exports);
|
|
21
|
+
__exportStar(require("./audiocall.schema"), exports);
|
|
22
|
+
__exportStar(require("./user.schema"), exports);
|
|
23
|
+
const message_schema_1 = require("./message.schema");
|
|
24
|
+
const room_schema_1 = require("./room.schema");
|
|
25
|
+
const presence_schema_1 = require("./presence.schema");
|
|
26
|
+
const audiocall_schema_1 = require("./audiocall.schema");
|
|
27
|
+
const user_schema_1 = require("./user.schema");
|
|
28
|
+
exports.schemas = [
|
|
29
|
+
{ name: 'Message', schema: message_schema_1.MessageSchema },
|
|
30
|
+
{ name: 'Room', schema: room_schema_1.RoomSchema },
|
|
31
|
+
{ name: 'Presence', schema: presence_schema_1.PresenceSchema },
|
|
32
|
+
{ name: 'AudioCall', schema: audiocall_schema_1.AudioCallSchema },
|
|
33
|
+
{ name: 'User', schema: user_schema_1.UserSchema },
|
|
34
|
+
];
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Message = exports.MessageSchema = void 0;
|
|
4
|
+
const mongoose_1 = require("mongoose");
|
|
5
|
+
const readReceiptSchema = new mongoose_1.Schema({
|
|
6
|
+
userId: { type: String, required: true },
|
|
7
|
+
readAt: { type: Date, required: true, default: Date.now }
|
|
8
|
+
});
|
|
9
|
+
exports.MessageSchema = new mongoose_1.Schema({
|
|
10
|
+
tenantId: { type: String, required: true, index: true },
|
|
11
|
+
roomId: {
|
|
12
|
+
type: String,
|
|
13
|
+
required: true,
|
|
14
|
+
index: true
|
|
15
|
+
},
|
|
16
|
+
senderId: {
|
|
17
|
+
type: String,
|
|
18
|
+
required: true,
|
|
19
|
+
index: true
|
|
20
|
+
},
|
|
21
|
+
senderName: { type: String, required: true },
|
|
22
|
+
senderAvatar: { type: String },
|
|
23
|
+
content: { type: String, required: true },
|
|
24
|
+
fileUrl: { type: String },
|
|
25
|
+
fileType: { type: String },
|
|
26
|
+
fileName: { type: String },
|
|
27
|
+
readBy: { type: [readReceiptSchema], default: [] }
|
|
28
|
+
}, {
|
|
29
|
+
timestamps: true
|
|
30
|
+
});
|
|
31
|
+
exports.MessageSchema.index({ tenantId: 1, roomId: 1, createdAt: -1 });
|
|
32
|
+
exports.MessageSchema.set('toJSON', {
|
|
33
|
+
virtuals: true,
|
|
34
|
+
transform: function (doc, ret) {
|
|
35
|
+
ret.id = ret._id;
|
|
36
|
+
delete ret.__v;
|
|
37
|
+
}
|
|
38
|
+
});
|
|
39
|
+
// Export model after schema declaration
|
|
40
|
+
exports.Message = (0, mongoose_1.model)('Message', exports.MessageSchema);
|