chattransfer 2.0.17 → 3.0.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/frontend/dist/assets/{browser-BzA-fxZ1.js → browser-_rMkfdfG.js} +1 -1
- package/frontend/dist/assets/{index-B2dOrakb.js → index-BTJ_Fb6p.js} +43 -43
- package/frontend/dist/assets/index-IvWY8A4p.css +1 -0
- package/frontend/dist/index.html +2 -2
- package/package.json +4 -3
- package/server/cli-options.js +9 -1
- package/server/index.js +20 -4
- package/server/room-manager.js +117 -25
- package/server/routes/batch-download.js +31 -0
- package/server/routes/index.js +14 -2
- package/server/routes/qr.js +10 -2
- package/server/services/batch-download-service.js +175 -0
- package/server/services/message-history-service.js +779 -13
- package/server/socket/index.js +592 -51
- package/server/utils/network.js +11 -1
- package/server/utils/runtime-options.js +30 -0
- package/frontend/dist/assets/index-DYJPnabS.css +0 -1
package/server/socket/index.js
CHANGED
|
@@ -5,11 +5,17 @@ const {
|
|
|
5
5
|
decorateGroupMessage,
|
|
6
6
|
decoratePrivateMessage
|
|
7
7
|
} = require('../services/message-service');
|
|
8
|
+
const { isLoopbackAddress } = require('../utils/network');
|
|
8
9
|
|
|
9
|
-
|
|
10
|
+
const DEFAULT_ROOM_ID = 'group_chat';
|
|
11
|
+
|
|
12
|
+
module.exports = function registerSocket(server, historyService, batchDownloadService, options = {}) {
|
|
10
13
|
if (!historyService) {
|
|
11
14
|
throw new Error('historyService is required');
|
|
12
15
|
}
|
|
16
|
+
if (!batchDownloadService) {
|
|
17
|
+
throw new Error('batchDownloadService is required');
|
|
18
|
+
}
|
|
13
19
|
|
|
14
20
|
const io = socketIo(server, {
|
|
15
21
|
cors: {
|
|
@@ -19,18 +25,168 @@ module.exports = function registerSocket(server, historyService) {
|
|
|
19
25
|
pingTimeout: 30000,
|
|
20
26
|
pingInterval: 25000
|
|
21
27
|
});
|
|
22
|
-
const
|
|
28
|
+
const presenceOfflineDebounceMs = Number.isFinite(options.presenceOfflineDebounceMs)
|
|
29
|
+
? Math.max(0, options.presenceOfflineDebounceMs)
|
|
30
|
+
: 8000;
|
|
31
|
+
const roomIdleRetentionDays = Number.isFinite(options.roomIdleRetentionDays)
|
|
32
|
+
? Math.max(0, options.roomIdleRetentionDays)
|
|
33
|
+
: 7;
|
|
34
|
+
const roomIdleCleanupIntervalMs = Number.isFinite(options.roomIdleCleanupIntervalMs)
|
|
35
|
+
&& options.roomIdleCleanupIntervalMs > 0
|
|
36
|
+
? options.roomIdleCleanupIntervalMs
|
|
37
|
+
: 6 * 60 * 60 * 1000;
|
|
38
|
+
const offlineTimers = new Map();
|
|
39
|
+
const joinAttempts = new Map();
|
|
40
|
+
let roomIdleCleanupTimer = null;
|
|
41
|
+
|
|
42
|
+
const socketRoomName = (roomId = DEFAULT_ROOM_ID) => (
|
|
43
|
+
roomId === DEFAULT_ROOM_ID ? 'group-chat' : `room:${roomId}`
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const broadcastRoomUserList = (roomId = DEFAULT_ROOM_ID) => {
|
|
47
|
+
io.to(socketRoomName(roomId)).emit('user_list_updated', {
|
|
48
|
+
roomId,
|
|
49
|
+
users: roomManager.listUsers(roomId)
|
|
50
|
+
});
|
|
51
|
+
};
|
|
23
52
|
|
|
24
|
-
const
|
|
25
|
-
const
|
|
53
|
+
const emitPresenceChanged = (type, user, roomId, sourceSocket = null) => {
|
|
54
|
+
const payload = { type, user, roomId, at: Date.now() };
|
|
26
55
|
|
|
27
|
-
|
|
56
|
+
if (type === 'online' && sourceSocket) {
|
|
57
|
+
sourceSocket.broadcast.to(socketRoomName(roomId)).emit('presence_changed', payload);
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
io.to(socketRoomName(roomId)).emit('presence_changed', payload);
|
|
28
62
|
};
|
|
29
63
|
|
|
30
|
-
const
|
|
31
|
-
|
|
32
|
-
|
|
64
|
+
const clearOfflineTimer = (roomId, username) => {
|
|
65
|
+
const key = `${roomId}:${username}`;
|
|
66
|
+
const timer = offlineTimers.get(key);
|
|
67
|
+
if (!timer) return;
|
|
68
|
+
|
|
69
|
+
clearTimeout(timer);
|
|
70
|
+
offlineTimers.delete(key);
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const clearRoomTimers = (roomId) => {
|
|
74
|
+
for (const [key, timer] of offlineTimers.entries()) {
|
|
75
|
+
if (!key.startsWith(`${roomId}:`)) continue;
|
|
76
|
+
clearTimeout(timer);
|
|
77
|
+
offlineTimers.delete(key);
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
const requireActiveRoom = (socket, roomId) => {
|
|
82
|
+
if (!roomId || roomId !== socket.activeRoomId) {
|
|
83
|
+
throw new Error('Join the room before managing it');
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
const scheduleOffline = ({ roomId, username }) => {
|
|
88
|
+
clearOfflineTimer(roomId, username);
|
|
89
|
+
const key = `${roomId}:${username}`;
|
|
90
|
+
const timer = setTimeout(() => {
|
|
91
|
+
offlineTimers.delete(key);
|
|
92
|
+
const currentUser = roomManager.getUser(roomId, username);
|
|
93
|
+
if (currentUser?.status === roomManager.getOnline() && currentUser.socketIds.size > 0) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
const result = roomManager.markOffline({ roomId, username });
|
|
97
|
+
if (!result.becameOffline) return;
|
|
98
|
+
|
|
99
|
+
broadcastRoomUserList(roomId);
|
|
100
|
+
emitPresenceChanged('offline', result.user, roomId);
|
|
101
|
+
}, presenceOfflineDebounceMs);
|
|
102
|
+
|
|
103
|
+
offlineTimers.set(key, timer);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const leaveActiveRoom = (socket, { immediateOffline = false } = {}) => {
|
|
107
|
+
const roomId = socket.activeRoomId;
|
|
108
|
+
if (!roomId) return;
|
|
109
|
+
|
|
110
|
+
socket.leave(socketRoomName(roomId));
|
|
111
|
+
socket.activeRoomId = null;
|
|
112
|
+
const result = roomManager.removeSocket({
|
|
113
|
+
roomId,
|
|
114
|
+
username: socket.username,
|
|
115
|
+
socketId: socket.id
|
|
33
116
|
});
|
|
117
|
+
|
|
118
|
+
if (result.user && !result.hasConnections && immediateOffline) {
|
|
119
|
+
clearOfflineTimer(roomId, socket.username);
|
|
120
|
+
const offline = roomManager.markOffline({ roomId, username: socket.username });
|
|
121
|
+
if (offline.becameOffline) {
|
|
122
|
+
broadcastRoomUserList(roomId);
|
|
123
|
+
emitPresenceChanged('offline', offline.user, roomId);
|
|
124
|
+
}
|
|
125
|
+
} else if (result.user && !result.hasConnections) {
|
|
126
|
+
scheduleOffline({ roomId, username: socket.username });
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const detachRoomSockets = (roomId) => {
|
|
131
|
+
const socketIds = io.sockets.adapter.rooms.get(socketRoomName(roomId));
|
|
132
|
+
if (socketIds) {
|
|
133
|
+
for (const socketId of [...socketIds]) {
|
|
134
|
+
const memberSocket = io.sockets.sockets.get(socketId);
|
|
135
|
+
if (!memberSocket) continue;
|
|
136
|
+
memberSocket.leave(socketRoomName(roomId));
|
|
137
|
+
if (memberSocket.activeRoomId === roomId) {
|
|
138
|
+
memberSocket.activeRoomId = null;
|
|
139
|
+
}
|
|
140
|
+
roomManager.removeSocket({
|
|
141
|
+
roomId,
|
|
142
|
+
username: memberSocket.username,
|
|
143
|
+
socketId: memberSocket.id
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
clearRoomTimers(roomId);
|
|
149
|
+
roomManager.removeRoom(roomId);
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
const registerUserOnline = async (socket, {
|
|
153
|
+
roomId = DEFAULT_ROOM_ID,
|
|
154
|
+
code = null,
|
|
155
|
+
nickname = '',
|
|
156
|
+
username = null
|
|
157
|
+
} = {}) => {
|
|
158
|
+
const userId = username || socket.username;
|
|
159
|
+
if (!userId) throw new Error('User is required');
|
|
160
|
+
|
|
161
|
+
socket.username = userId;
|
|
162
|
+
const joinedRoom = await historyService.joinRoom({ roomId, code, userId, nickname });
|
|
163
|
+
leaveActiveRoom(socket);
|
|
164
|
+
|
|
165
|
+
socket.join(socketRoomName(roomId));
|
|
166
|
+
socket.activeRoomId = roomId;
|
|
167
|
+
const result = roomManager.setOnline({
|
|
168
|
+
roomId,
|
|
169
|
+
username: userId,
|
|
170
|
+
socketId: socket.id,
|
|
171
|
+
nickname,
|
|
172
|
+
ip: getClientIp(socket)
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const room = joinedRoom || await historyService.getRoom(roomId);
|
|
176
|
+
if (room) roomManager.setRoomName(roomId, room.name);
|
|
177
|
+
broadcastRoomUserList(roomId);
|
|
178
|
+
|
|
179
|
+
if (result.becameOnline) {
|
|
180
|
+
emitPresenceChanged('online', result.user, roomId, socket);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return { ...result, room };
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const getClientIp = (socket) => {
|
|
187
|
+
const address = socket.handshake.address || socket.request.connection.remoteAddress;
|
|
188
|
+
|
|
189
|
+
return address ? address.replace(/^::ffff:/, '') : '';
|
|
34
190
|
};
|
|
35
191
|
|
|
36
192
|
const trackMessage = (type, direction, params, socket) => {
|
|
@@ -53,18 +209,76 @@ module.exports = function registerSocket(server, historyService) {
|
|
|
53
209
|
}
|
|
54
210
|
};
|
|
55
211
|
|
|
56
|
-
const broadcastHistoryCleared = ({ chatType, targetUser, sessionId }, sourceSocket) => {
|
|
212
|
+
const broadcastHistoryCleared = ({ roomId = null, chatType = null, targetUser = null, sessionId = null }, sourceSocket) => {
|
|
213
|
+
if (roomId) {
|
|
214
|
+
io.to(socketRoomName(roomId)).emit('history_cleared', {
|
|
215
|
+
roomId,
|
|
216
|
+
chatType: 'room',
|
|
217
|
+
sessionId
|
|
218
|
+
});
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
|
|
57
222
|
const payload = { chatType, targetUser, sessionId };
|
|
58
223
|
|
|
59
224
|
if (chatType === 'group') {
|
|
60
|
-
io.to(
|
|
225
|
+
io.to(socketRoomName(DEFAULT_ROOM_ID)).emit('history_cleared', payload);
|
|
61
226
|
return;
|
|
62
227
|
}
|
|
63
228
|
|
|
64
229
|
sourceSocket.emit('history_cleared', payload);
|
|
65
|
-
const receiver = roomManager.getUser(targetUser);
|
|
230
|
+
const receiver = roomManager.getUser(DEFAULT_ROOM_ID, targetUser);
|
|
66
231
|
if (receiver && receiver.status === roomManager.getOnline()) {
|
|
67
|
-
|
|
232
|
+
for (const socketId of receiver.socketIds) {
|
|
233
|
+
io.to(socketId).emit('history_cleared', payload);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
|
|
238
|
+
const checkJoinRateLimit = (socket) => {
|
|
239
|
+
const key = socket.handshake.address || socket.id;
|
|
240
|
+
const now = Date.now();
|
|
241
|
+
const attempts = (joinAttempts.get(key) || []).filter((at) => now - at < 60000);
|
|
242
|
+
|
|
243
|
+
if (attempts.length >= 10) {
|
|
244
|
+
throw new Error('Too many join attempts');
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
attempts.push(now);
|
|
248
|
+
joinAttempts.set(key, attempts);
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const runIdleRoomCleanup = async () => {
|
|
252
|
+
if (roomIdleRetentionDays <= 0) return;
|
|
253
|
+
|
|
254
|
+
try {
|
|
255
|
+
const now = Date.now();
|
|
256
|
+
const cutoff = now - (roomIdleRetentionDays * 24 * 60 * 60 * 1000);
|
|
257
|
+
const candidates = await historyService.listInactiveRooms({
|
|
258
|
+
retentionDays: roomIdleRetentionDays,
|
|
259
|
+
now
|
|
260
|
+
});
|
|
261
|
+
for (const room of candidates) {
|
|
262
|
+
const hasOnlineUsers = roomManager.listUsers(room.id)
|
|
263
|
+
.some((user) => user.status === roomManager.getOnline());
|
|
264
|
+
if (hasOnlineUsers) continue;
|
|
265
|
+
|
|
266
|
+
const deleted = await historyService.deleteRoomBySystem({
|
|
267
|
+
roomId: room.id,
|
|
268
|
+
reason: 'inactive',
|
|
269
|
+
cutoff
|
|
270
|
+
});
|
|
271
|
+
if (!deleted) continue;
|
|
272
|
+
|
|
273
|
+
batchDownloadService.revokeRoom(room.id);
|
|
274
|
+
io.to(socketRoomName(room.id)).emit('room_deleted', {
|
|
275
|
+
roomId: room.id,
|
|
276
|
+
reason: 'inactive'
|
|
277
|
+
});
|
|
278
|
+
detachRoomSockets(room.id);
|
|
279
|
+
}
|
|
280
|
+
} catch (error) {
|
|
281
|
+
console.error('[Room] idle cleanup failed:', error.message);
|
|
68
282
|
}
|
|
69
283
|
};
|
|
70
284
|
|
|
@@ -73,22 +287,194 @@ module.exports = function registerSocket(server, historyService) {
|
|
|
73
287
|
|
|
74
288
|
socket.on('login', ({ uid } = {}) => {
|
|
75
289
|
console.log(`[Socket] login: ${uid}`);
|
|
290
|
+
if (uid) socket.username = uid;
|
|
76
291
|
});
|
|
77
292
|
|
|
78
|
-
socket.on('online', (username) => {
|
|
293
|
+
socket.on('online', async (username) => {
|
|
79
294
|
console.log(`[Socket] online: ${username} ${socket.id}`);
|
|
80
|
-
socket
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
295
|
+
await registerUserOnline(socket, { roomId: DEFAULT_ROOM_ID, username });
|
|
296
|
+
});
|
|
297
|
+
|
|
298
|
+
socket.on('room_online', async (params = {}, ack) => {
|
|
299
|
+
try {
|
|
300
|
+
const result = await registerUserOnline(socket, params);
|
|
301
|
+
ack?.({ code: 0, data: { room: result.room } });
|
|
302
|
+
} catch (error) {
|
|
303
|
+
console.error('[Room] online failed:', error.message);
|
|
304
|
+
ack?.({ code: -1, message: error.message });
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
socket.on('room_create', async (params = {}, ack) => {
|
|
309
|
+
if (typeof ack !== 'function') return;
|
|
310
|
+
|
|
311
|
+
try {
|
|
312
|
+
if (!socket.username) throw new Error('User is required');
|
|
313
|
+
const room = await historyService.createRoom({
|
|
314
|
+
name: params.name,
|
|
315
|
+
createdBy: socket.username,
|
|
316
|
+
nickname: params.nickname || ''
|
|
317
|
+
});
|
|
318
|
+
await historyService.joinRoom({
|
|
319
|
+
roomId: room.id,
|
|
320
|
+
code: room.code,
|
|
321
|
+
userId: socket.username,
|
|
322
|
+
nickname: params.nickname || ''
|
|
323
|
+
});
|
|
324
|
+
roomManager.setRoomName(room.id, room.name);
|
|
325
|
+
|
|
326
|
+
ack({ code: 0, data: { room } });
|
|
327
|
+
} catch (error) {
|
|
328
|
+
console.error('[Room] create failed:', error.message);
|
|
329
|
+
ack({ code: -1, message: error.message });
|
|
330
|
+
}
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
socket.on('room_join_by_code', async (params = {}, ack) => {
|
|
334
|
+
if (typeof ack !== 'function') return;
|
|
335
|
+
|
|
336
|
+
try {
|
|
337
|
+
if (!socket.username) throw new Error('User is required');
|
|
338
|
+
checkJoinRateLimit(socket);
|
|
339
|
+
const room = await historyService.getRoomByCode(params.code);
|
|
340
|
+
if (!room) throw new Error('Room not found');
|
|
341
|
+
|
|
342
|
+
await historyService.joinRoom({
|
|
343
|
+
roomId: room.id,
|
|
344
|
+
code: params.code,
|
|
345
|
+
userId: socket.username,
|
|
346
|
+
nickname: params.nickname || ''
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
ack({ code: 0, data: { room } });
|
|
350
|
+
} catch (error) {
|
|
351
|
+
console.error('[Room] join failed:', error.message);
|
|
352
|
+
ack({ code: -1, message: error.message });
|
|
353
|
+
}
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
socket.on('room_list', async (params = {}, ack) => {
|
|
357
|
+
if (typeof ack !== 'function') return;
|
|
358
|
+
|
|
359
|
+
try {
|
|
360
|
+
if (!socket.username) throw new Error('User is required');
|
|
361
|
+
const rooms = await historyService.listRoomsForUser(socket.username);
|
|
362
|
+
const data = rooms.map((room) => ({
|
|
363
|
+
...room,
|
|
364
|
+
onlineCount: roomManager.listUsers(room.id)
|
|
365
|
+
.filter((user) => user.status === roomManager.getOnline())
|
|
366
|
+
.length
|
|
367
|
+
}));
|
|
368
|
+
|
|
369
|
+
ack({ code: 0, data });
|
|
370
|
+
} catch (error) {
|
|
371
|
+
console.error('[Room] list failed:', error.message);
|
|
372
|
+
ack({ code: -1, message: error.message });
|
|
373
|
+
}
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
socket.on('room_update_name', async (params = {}, ack) => {
|
|
377
|
+
if (typeof ack !== 'function') return;
|
|
378
|
+
|
|
379
|
+
try {
|
|
380
|
+
const roomId = params.roomId || socket.activeRoomId;
|
|
381
|
+
requireActiveRoom(socket, roomId);
|
|
382
|
+
const room = await historyService.updateRoomName({
|
|
383
|
+
roomId,
|
|
384
|
+
name: params.name,
|
|
385
|
+
requester: socket.username
|
|
386
|
+
});
|
|
387
|
+
roomManager.setRoomName(roomId, room.name);
|
|
388
|
+
io.to(socketRoomName(roomId)).emit('room_updated', { room });
|
|
389
|
+
|
|
390
|
+
ack({ code: 0, data: { room } });
|
|
391
|
+
} catch (error) {
|
|
392
|
+
console.error('[Room] rename failed:', error.message);
|
|
393
|
+
ack({ code: -1, message: error.message });
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
socket.on('room_regenerate_code', async (params = {}, ack) => {
|
|
398
|
+
if (typeof ack !== 'function') return;
|
|
399
|
+
|
|
400
|
+
try {
|
|
401
|
+
const roomId = params.roomId || socket.activeRoomId;
|
|
402
|
+
requireActiveRoom(socket, roomId);
|
|
403
|
+
const room = await historyService.regenerateRoomCode({
|
|
404
|
+
roomId,
|
|
405
|
+
requester: socket.username
|
|
406
|
+
});
|
|
407
|
+
io.to(socketRoomName(roomId)).emit('room_updated', { room });
|
|
408
|
+
|
|
409
|
+
ack({ code: 0, data: { room } });
|
|
410
|
+
} catch (error) {
|
|
411
|
+
console.error('[Room] regenerate code failed:', error.message);
|
|
412
|
+
ack({ code: -1, message: error.message });
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
socket.on('room_leave', async (params = {}, ack) => {
|
|
417
|
+
if (typeof ack !== 'function') return;
|
|
418
|
+
|
|
419
|
+
try {
|
|
420
|
+
const roomId = params.roomId || socket.activeRoomId;
|
|
421
|
+
requireActiveRoom(socket, roomId);
|
|
422
|
+
const data = await historyService.leaveRoom({
|
|
423
|
+
roomId,
|
|
424
|
+
userId: socket.username
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
if (data.dissolved) {
|
|
428
|
+
batchDownloadService.revokeRoom(roomId);
|
|
429
|
+
io.to(socketRoomName(roomId)).emit('room_deleted', {
|
|
430
|
+
roomId,
|
|
431
|
+
reason: 'last_member_left'
|
|
432
|
+
});
|
|
433
|
+
detachRoomSockets(roomId);
|
|
434
|
+
} else {
|
|
435
|
+
if (data.ownerChanged) {
|
|
436
|
+
io.to(socketRoomName(roomId)).emit('room_owner_changed', {
|
|
437
|
+
roomId,
|
|
438
|
+
ownerId: data.newOwnerId
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
leaveActiveRoom(socket, { immediateOffline: true });
|
|
442
|
+
broadcastRoomUserList(roomId);
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
ack({ code: 0, data });
|
|
446
|
+
} catch (error) {
|
|
447
|
+
console.error('[Room] leave failed:', error.message);
|
|
448
|
+
ack({ code: -1, message: error.message });
|
|
449
|
+
}
|
|
450
|
+
});
|
|
451
|
+
|
|
452
|
+
socket.on('room_dissolve', async (params = {}, ack) => {
|
|
453
|
+
if (typeof ack !== 'function') return;
|
|
454
|
+
|
|
455
|
+
try {
|
|
456
|
+
const roomId = params.roomId || socket.activeRoomId;
|
|
457
|
+
requireActiveRoom(socket, roomId);
|
|
458
|
+
const data = await historyService.dissolveRoom({
|
|
459
|
+
roomId,
|
|
460
|
+
requester: socket.username
|
|
461
|
+
});
|
|
462
|
+
|
|
463
|
+
batchDownloadService.revokeRoom(roomId);
|
|
464
|
+
io.to(socketRoomName(roomId)).emit('room_deleted', {
|
|
465
|
+
roomId,
|
|
466
|
+
reason: 'owner_dissolved'
|
|
467
|
+
});
|
|
468
|
+
detachRoomSockets(roomId);
|
|
469
|
+
ack({ code: 0, data });
|
|
470
|
+
} catch (error) {
|
|
471
|
+
console.error('[Room] dissolve failed:', error.message);
|
|
472
|
+
ack({ code: -1, message: error.message });
|
|
473
|
+
}
|
|
88
474
|
});
|
|
89
475
|
|
|
90
476
|
socket.on('private_chat', async (params, ack) => {
|
|
91
|
-
const decorated = decoratePrivateMessage(params, roomManager.getUser(params.sender));
|
|
477
|
+
const decorated = decoratePrivateMessage(params, roomManager.getUser(DEFAULT_ROOM_ID, params.sender));
|
|
92
478
|
const { message: savedMessage, error } = await persistMessage(decorated);
|
|
93
479
|
|
|
94
480
|
if (typeof ack === 'function') {
|
|
@@ -99,10 +485,11 @@ module.exports = function registerSocket(server, historyService) {
|
|
|
99
485
|
|
|
100
486
|
trackMessage('private', 'send', decorated, socket);
|
|
101
487
|
|
|
102
|
-
const receiver = roomManager.getUser(savedMessage.receiver);
|
|
103
|
-
|
|
488
|
+
const receiver = roomManager.getUser(DEFAULT_ROOM_ID, savedMessage.receiver);
|
|
104
489
|
if (receiver && receiver.status === roomManager.getOnline()) {
|
|
105
|
-
|
|
490
|
+
for (const socketId of receiver.socketIds) {
|
|
491
|
+
io.to(socketId).emit('reply_private_chat', savedMessage);
|
|
492
|
+
}
|
|
106
493
|
|
|
107
494
|
trackEvent('message_action', {
|
|
108
495
|
chat_type: 'private',
|
|
@@ -118,36 +505,36 @@ module.exports = function registerSocket(server, historyService) {
|
|
|
118
505
|
|
|
119
506
|
socket.on('group_online', (username, nickname) => {
|
|
120
507
|
console.log(`[Socket] group_online: ${username} ${socket.id} nickname=${nickname}`);
|
|
121
|
-
socket
|
|
122
|
-
|
|
123
|
-
roomManager.setOnline({
|
|
508
|
+
registerUserOnline(socket, {
|
|
509
|
+
roomId: DEFAULT_ROOM_ID,
|
|
124
510
|
username,
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
});
|
|
133
|
-
broadcastUserList();
|
|
511
|
+
nickname
|
|
512
|
+
}).then(() => {
|
|
513
|
+
socket.emit('group_name_updated', {
|
|
514
|
+
groupName: roomManager.getRoomName(DEFAULT_ROOM_ID),
|
|
515
|
+
updatedBy: ''
|
|
516
|
+
});
|
|
517
|
+
}).catch((error) => console.error('[Room] legacy online failed:', error.message));
|
|
134
518
|
});
|
|
135
519
|
|
|
136
520
|
socket.on('update_nickname', ({ sender, nickname }) => {
|
|
137
|
-
roomManager.setNickname(
|
|
138
|
-
|
|
521
|
+
roomManager.setNickname({
|
|
522
|
+
roomId: socket.activeRoomId || DEFAULT_ROOM_ID,
|
|
523
|
+
username: sender,
|
|
524
|
+
nickname
|
|
525
|
+
});
|
|
526
|
+
broadcastRoomUserList(socket.activeRoomId || DEFAULT_ROOM_ID);
|
|
139
527
|
});
|
|
140
528
|
|
|
141
529
|
socket.on('update_group_name', ({ groupName, updatedBy }) => {
|
|
142
|
-
|
|
143
|
-
io.to(chatRoom).emit('group_name_updated', {
|
|
144
|
-
groupName: roomManager.getGroupName(),
|
|
145
|
-
updatedBy: updatedBy || ''
|
|
146
|
-
});
|
|
530
|
+
console.warn('[Room] Public Lobby name cannot be changed');
|
|
147
531
|
});
|
|
148
532
|
|
|
149
533
|
socket.on('group_chat', async (params, ack) => {
|
|
150
|
-
const decorated = decorateGroupMessage(
|
|
534
|
+
const decorated = decorateGroupMessage(
|
|
535
|
+
params,
|
|
536
|
+
roomManager.getUser(DEFAULT_ROOM_ID, params.sender)
|
|
537
|
+
);
|
|
151
538
|
const { message: savedMessage, error } = await persistMessage(decorated);
|
|
152
539
|
|
|
153
540
|
if (typeof ack === 'function') {
|
|
@@ -157,7 +544,7 @@ module.exports = function registerSocket(server, historyService) {
|
|
|
157
544
|
if (error) return;
|
|
158
545
|
|
|
159
546
|
trackMessage('group', 'send', decorated, socket);
|
|
160
|
-
socket.broadcast.to(
|
|
547
|
+
socket.broadcast.to(socketRoomName(DEFAULT_ROOM_ID)).emit('reply_group_chat', savedMessage);
|
|
161
548
|
|
|
162
549
|
trackEvent('message_action', {
|
|
163
550
|
chat_type: 'group',
|
|
@@ -167,16 +554,57 @@ module.exports = function registerSocket(server, historyService) {
|
|
|
167
554
|
});
|
|
168
555
|
});
|
|
169
556
|
|
|
557
|
+
socket.on('room_chat', async (params = {}, ack) => {
|
|
558
|
+
try {
|
|
559
|
+
const roomId = params.roomId || socket.activeRoomId;
|
|
560
|
+
if (!roomId || roomId !== socket.activeRoomId) {
|
|
561
|
+
throw new Error('Join the room before sending messages');
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
const messageData = {
|
|
565
|
+
...params,
|
|
566
|
+
roomId,
|
|
567
|
+
receiver: roomId
|
|
568
|
+
};
|
|
569
|
+
const decorated = decorateGroupMessage(
|
|
570
|
+
messageData,
|
|
571
|
+
roomManager.getUser(roomId, params.sender)
|
|
572
|
+
);
|
|
573
|
+
const { message: savedMessage, error } = await persistMessage(decorated);
|
|
574
|
+
|
|
575
|
+
if (typeof ack === 'function') {
|
|
576
|
+
ack(error ? { ...decorated, status: 'failed', error: 'Message persistence failed' } : savedMessage);
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
if (error) return;
|
|
580
|
+
|
|
581
|
+
trackMessage('room', 'send', decorated, socket);
|
|
582
|
+
socket.broadcast.to(socketRoomName(roomId)).emit('reply_room_chat', savedMessage);
|
|
583
|
+
|
|
584
|
+
trackEvent('message_action', {
|
|
585
|
+
chat_type: 'room',
|
|
586
|
+
msg_type: decorated.msgType || 'TEXT',
|
|
587
|
+
direction: 'broadcast',
|
|
588
|
+
user_agent: socket.handshake.headers['user-agent']
|
|
589
|
+
});
|
|
590
|
+
} catch (error) {
|
|
591
|
+
console.error('[Room] message failed:', error.message);
|
|
592
|
+
ack?.({ status: 'failed', error: error.message });
|
|
593
|
+
}
|
|
594
|
+
});
|
|
595
|
+
|
|
170
596
|
socket.on('history_query', async (params = {}, ack) => {
|
|
171
597
|
if (typeof ack !== 'function') return;
|
|
172
598
|
|
|
173
599
|
try {
|
|
174
600
|
const requester = socket.username || params.requester;
|
|
601
|
+
const roomId = params.roomId;
|
|
175
602
|
const chatType = params.chatType || (
|
|
176
603
|
params.targetUser === 'group_chat' ? 'group' : 'private'
|
|
177
604
|
);
|
|
178
605
|
const data = await historyService.queryHistory({
|
|
179
606
|
requester,
|
|
607
|
+
roomId,
|
|
180
608
|
chatType,
|
|
181
609
|
targetUser: params.targetUser,
|
|
182
610
|
cursor: params.cursor,
|
|
@@ -190,29 +618,89 @@ module.exports = function registerSocket(server, historyService) {
|
|
|
190
618
|
}
|
|
191
619
|
});
|
|
192
620
|
|
|
193
|
-
socket.on('
|
|
621
|
+
socket.on('attachment_query', async (params = {}, ack) => {
|
|
194
622
|
if (typeof ack !== 'function') return;
|
|
195
623
|
|
|
196
624
|
try {
|
|
197
625
|
const requester = socket.username || params.requester;
|
|
626
|
+
const roomId = params.roomId;
|
|
198
627
|
const chatType = params.chatType || (
|
|
199
628
|
params.targetUser === 'group_chat' ? 'group' : 'private'
|
|
200
629
|
);
|
|
201
|
-
const
|
|
630
|
+
const { attachments } = await historyService.queryAttachments({
|
|
202
631
|
requester,
|
|
632
|
+
roomId,
|
|
203
633
|
chatType,
|
|
204
634
|
targetUser: params.targetUser
|
|
205
635
|
});
|
|
636
|
+
|
|
637
|
+
ack({
|
|
638
|
+
code: 0,
|
|
639
|
+
data: {
|
|
640
|
+
attachments: attachments.map((attachment) => ({
|
|
641
|
+
msgId: attachment.msgId,
|
|
642
|
+
seq: attachment.seq,
|
|
643
|
+
sender: attachment.sender,
|
|
644
|
+
senderNickname: attachment.senderNickname,
|
|
645
|
+
msgType: attachment.msgType,
|
|
646
|
+
createTime: attachment.create_time || attachment.createTime,
|
|
647
|
+
createdAt: attachment.createdAt,
|
|
648
|
+
fileName: attachment.fileName,
|
|
649
|
+
fileSize: attachment.fileSize,
|
|
650
|
+
fileType: attachment.fileType,
|
|
651
|
+
fileUrl: attachment.fileUrl,
|
|
652
|
+
available: attachment.available
|
|
653
|
+
}))
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
} catch (error) {
|
|
657
|
+
console.error('[Attachment] query failed:', error.message);
|
|
658
|
+
ack({ code: -1, message: 'Failed to load attachments' });
|
|
659
|
+
}
|
|
660
|
+
});
|
|
661
|
+
|
|
662
|
+
socket.on('attachment_download', async (params = {}, ack) => {
|
|
663
|
+
if (typeof ack !== 'function') return;
|
|
664
|
+
|
|
665
|
+
try {
|
|
666
|
+
const requester = socket.username || params.requester;
|
|
667
|
+
const data = await batchDownloadService.createDownloadToken({
|
|
668
|
+
requester,
|
|
669
|
+
roomId: params.roomId,
|
|
670
|
+
chatType: params.chatType,
|
|
671
|
+
targetUser: params.targetUser,
|
|
672
|
+
messageIds: params.messageIds
|
|
673
|
+
});
|
|
674
|
+
|
|
675
|
+
ack({ code: 0, data });
|
|
676
|
+
} catch (error) {
|
|
677
|
+
console.error('[Attachment] download request failed:', error.message);
|
|
678
|
+
ack({ code: -1, message: error.message });
|
|
679
|
+
}
|
|
680
|
+
});
|
|
681
|
+
|
|
682
|
+
socket.on('history_clear', async (params = {}, ack) => {
|
|
683
|
+
if (typeof ack !== 'function') return;
|
|
684
|
+
|
|
685
|
+
try {
|
|
686
|
+
const requester = socket.username || params.requester;
|
|
687
|
+
const roomId = params.roomId;
|
|
688
|
+
if (roomId) requireActiveRoom(socket, roomId);
|
|
689
|
+
const chatType = params.chatType || (
|
|
690
|
+
params.targetUser === 'group_chat' ? 'group' : 'private'
|
|
691
|
+
);
|
|
206
692
|
const data = await historyService.clearHistory({
|
|
207
693
|
requester,
|
|
694
|
+
roomId,
|
|
208
695
|
chatType,
|
|
209
696
|
targetUser: params.targetUser
|
|
210
697
|
});
|
|
211
698
|
|
|
212
699
|
broadcastHistoryCleared({
|
|
700
|
+
roomId,
|
|
213
701
|
chatType,
|
|
214
702
|
targetUser: params.targetUser,
|
|
215
|
-
sessionId:
|
|
703
|
+
sessionId: data.sessionId
|
|
216
704
|
}, socket);
|
|
217
705
|
ack({ code: 0, data });
|
|
218
706
|
} catch (error) {
|
|
@@ -221,12 +709,65 @@ module.exports = function registerSocket(server, historyService) {
|
|
|
221
709
|
}
|
|
222
710
|
});
|
|
223
711
|
|
|
712
|
+
socket.on('public_room_admin_clear', async (params = {}, ack) => {
|
|
713
|
+
if (typeof ack !== 'function') return;
|
|
714
|
+
|
|
715
|
+
try {
|
|
716
|
+
const clientIp = getClientIp(socket);
|
|
717
|
+
if (!isLoopbackAddress(clientIp)) {
|
|
718
|
+
throw new Error('Administrator access is only available from localhost');
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
const data = await historyService.clearPublicRoomHistory();
|
|
722
|
+
broadcastHistoryCleared({
|
|
723
|
+
roomId: DEFAULT_ROOM_ID,
|
|
724
|
+
sessionId: data.sessionId
|
|
725
|
+
}, socket);
|
|
726
|
+
ack({ code: 0, data });
|
|
727
|
+
} catch (error) {
|
|
728
|
+
console.error('[History] public room admin clear failed:', error.message);
|
|
729
|
+
ack({ code: -1, message: error.message });
|
|
730
|
+
}
|
|
731
|
+
});
|
|
732
|
+
|
|
224
733
|
socket.on('disconnect', () => {
|
|
225
734
|
console.log(`[Socket] disconnect: ${socket.username || 'unknown'} ${socket.id}`);
|
|
226
|
-
|
|
227
|
-
|
|
735
|
+
if (!socket.activeRoomId || !socket.username) return;
|
|
736
|
+
|
|
737
|
+
const result = roomManager.removeSocket({
|
|
738
|
+
roomId: socket.activeRoomId,
|
|
739
|
+
username: socket.username,
|
|
740
|
+
socketId: socket.id
|
|
741
|
+
});
|
|
742
|
+
|
|
743
|
+
if (result.user && !result.hasConnections) {
|
|
744
|
+
scheduleOffline({
|
|
745
|
+
roomId: socket.activeRoomId,
|
|
746
|
+
username: socket.username
|
|
747
|
+
});
|
|
748
|
+
}
|
|
228
749
|
});
|
|
229
750
|
});
|
|
230
751
|
|
|
752
|
+
const idleCleanupReady = roomIdleRetentionDays > 0
|
|
753
|
+
? runIdleRoomCleanup()
|
|
754
|
+
: Promise.resolve();
|
|
755
|
+
if (roomIdleRetentionDays > 0) {
|
|
756
|
+
roomIdleCleanupTimer = setInterval(() => {
|
|
757
|
+
void runIdleRoomCleanup();
|
|
758
|
+
}, roomIdleCleanupIntervalMs);
|
|
759
|
+
roomIdleCleanupTimer.unref?.();
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
io.on('close', () => {
|
|
763
|
+
for (const timer of offlineTimers.values()) clearTimeout(timer);
|
|
764
|
+
offlineTimers.clear();
|
|
765
|
+
if (roomIdleCleanupTimer) {
|
|
766
|
+
clearInterval(roomIdleCleanupTimer);
|
|
767
|
+
roomIdleCleanupTimer = null;
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
|
|
771
|
+
io.idleCleanupReady = idleCleanupReady;
|
|
231
772
|
return io;
|
|
232
773
|
};
|