chattransfer 2.0.18 → 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.
@@ -5,8 +5,8 @@
5
5
  <link rel="icon" href="/assets/logo-DQUxWwh1.png" />
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <title>ChatTransfer</title>
8
- <script type="module" crossorigin src="/assets/index-PrmIL31V.js"></script>
9
- <link rel="stylesheet" crossorigin href="/assets/index-DDcqIagG.css">
8
+ <script type="module" crossorigin src="/assets/index-BTJ_Fb6p.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-IvWY8A4p.css">
10
10
  </head>
11
11
  <body>
12
12
  <div id="app"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "chattransfer",
3
- "version": "2.0.18",
3
+ "version": "3.0.0",
4
4
  "description": "LAN instant messaging and file transfer tool",
5
5
  "main": "server/index.js",
6
6
  "engines": {
@@ -5,6 +5,8 @@ const CLI_OPTIONS = {
5
5
  port: { type: 'string' },
6
6
  'data-dir': { type: 'string' },
7
7
  'public-url': { type: 'string' },
8
+ 'room-idle-retention-days': { type: 'string' },
9
+ 'public-room-retention-days': { type: 'string' },
8
10
  'no-browser': { type: 'boolean', default: false },
9
11
  version: { type: 'boolean', default: false },
10
12
  help: { type: 'boolean', default: false }
@@ -33,6 +35,8 @@ function toStartOptions(cliValues, env = process.env) {
33
35
  port: cliValues.port,
34
36
  dataDir: cliValues['data-dir'],
35
37
  publicUrl: cliValues['public-url'],
38
+ roomIdleRetentionDays: cliValues['room-idle-retention-days'],
39
+ publicRoomRetentionDays: cliValues['public-room-retention-days'],
36
40
  openBrowser: cliValues['no-browser'] ? false : undefined
37
41
  }, env);
38
42
  }
@@ -49,6 +53,8 @@ function getHelpText() {
49
53
  ' --port <port> Listen port (default: 55556)',
50
54
  ' --data-dir <path> Runtime data directory',
51
55
  ' --public-url <url> Public URL used by QR codes and links',
56
+ ' --room-idle-retention-days <days> Delete idle rooms after N days (default: 7, 0 disables)',
57
+ ' --public-room-retention-days <days> Clear Public Lobby messages after N days (default: 7, 0 disables)',
52
58
  ' --no-browser Do not open the local browser',
53
59
  ' --version Print the version',
54
60
  ' --help Print this help',
@@ -58,7 +64,9 @@ function getHelpText() {
58
64
  ' CHATTRANSFER_PORT',
59
65
  ' CHATTRANSFER_DATA_DIR',
60
66
  ' CHATTRANSFER_PUBLIC_URL',
61
- ' CHATTRANSFER_NO_BROWSER'
67
+ ' CHATTRANSFER_NO_BROWSER',
68
+ ' CHATTRANSFER_ROOM_IDLE_RETENTION_DAYS',
69
+ ' CHATTRANSFER_PUBLIC_ROOM_RETENTION_DAYS'
62
70
  ].join('\n');
63
71
  }
64
72
 
package/server/index.js CHANGED
@@ -125,16 +125,29 @@ async function start(options = {}) {
125
125
  return;
126
126
  }
127
127
 
128
+ historyService.configureIdleRetention(runtimeOptions.roomIdleRetentionDays);
129
+ const publicRoomRetentionDays = runtimeOptions.publicRoomRetentionDays;
130
+ if (Number.isFinite(publicRoomRetentionDays) && publicRoomRetentionDays > 0) {
131
+ historyService.startPublicRoomCleanup({ retentionDays: publicRoomRetentionDays });
132
+ }
133
+
128
134
  const app = createConfiguredApp(uploadsDir, historyService, batchDownloadService);
129
135
  const server = http.createServer(app);
130
136
 
131
- registerSocket(server, historyService, batchDownloadService);
137
+ const io = registerSocket(server, historyService, batchDownloadService, {
138
+ roomIdleRetentionDays: runtimeOptions.roomIdleRetentionDays
139
+ });
140
+ await io.idleCleanupReady;
132
141
 
133
142
  await new Promise((resolve, reject) => {
134
143
  server.once('error', reject);
135
144
  server.listen(port, runtimeOptions.host, resolve);
136
145
  });
137
146
 
147
+ server.once('close', () => {
148
+ historyService.stopPublicRoomCleanup();
149
+ });
150
+
138
151
  const homeUrl = runtimeOptions.publicUrl || `http://${clientIpAddress}:${port}`;
139
152
 
140
153
  console.log(`ChatTransfer v${pkg.version}`);
@@ -143,6 +156,7 @@ async function start(options = {}) {
143
156
  console.log(`CacheDir: ${uploadsDir}`);
144
157
  console.log(`Runtime: ${getRuntimeName()}`);
145
158
  console.log(`DataDir: ${path.dirname(getConfigFile())}`);
159
+ console.log(`Retention: idle-room=${runtimeOptions.roomIdleRetentionDays}d public-messages=${runtimeOptions.publicRoomRetentionDays}d`);
146
160
 
147
161
  trackEvent('app_start', {
148
162
  local_url: homeUrl,
@@ -1,10 +1,20 @@
1
1
  const ONLINE = 'ONLINE';
2
2
  const OFFLINE = 'OFFLINE';
3
+ const DEFAULT_ROOM_ID = 'group_chat';
3
4
 
4
5
  class RoomManager {
5
6
  constructor() {
6
- this.users = new Map();
7
- this.groupName = '';
7
+ this.rooms = new Map();
8
+ this.roomNames = new Map();
9
+ this.ensureRoom(DEFAULT_ROOM_ID);
10
+ }
11
+
12
+ ensureRoom(roomId = DEFAULT_ROOM_ID) {
13
+ if (!this.rooms.has(roomId)) {
14
+ this.rooms.set(roomId, { users: new Map() });
15
+ }
16
+
17
+ return this.rooms.get(roomId);
8
18
  }
9
19
 
10
20
  getOnline() {
@@ -15,22 +25,55 @@ class RoomManager {
15
25
  return OFFLINE;
16
26
  }
17
27
 
18
- setOnline({ username, socketId, nickname = '', ip = '' }) {
19
- const existing = this.users.get(username) || {};
20
- this.users.set(username, {
21
- socketId,
28
+ setOnline({ roomId = DEFAULT_ROOM_ID, username, socketId, nickname = '', ip = '' }) {
29
+ const room = this.ensureRoom(roomId);
30
+ const existing = room.users.get(username) || {
31
+ socketIds: new Set(),
32
+ status: OFFLINE,
33
+ nickname: '',
34
+ ip: '',
35
+ lastSeenAt: null,
36
+ onlineSince: null
37
+ };
38
+ const socketIds = new Set(existing.socketIds || []);
39
+ if (socketId) socketIds.add(socketId);
40
+ const wasOnline = existing.status === ONLINE && socketIds.size > 0;
41
+ const user = {
42
+ ...existing,
43
+ socketIds,
22
44
  status: ONLINE,
23
45
  nickname: nickname || existing.nickname || '',
24
- ip
25
- });
46
+ ip: ip || existing.ip || '',
47
+ onlineSince: wasOnline ? existing.onlineSince : Date.now()
48
+ };
49
+ room.users.set(username, user);
50
+
51
+ return {
52
+ user: this.getPublicUser(roomId, username),
53
+ becameOnline: !wasOnline
54
+ };
26
55
  }
27
56
 
28
- getUser(username) {
29
- return this.users.get(username);
57
+ getUser(roomId = DEFAULT_ROOM_ID, username) {
58
+ return this.ensureRoom(roomId).users.get(username);
59
+ }
60
+
61
+ getPublicUser(roomId = DEFAULT_ROOM_ID, username) {
62
+ const user = this.ensureRoom(roomId).users.get(username);
63
+ if (!user) return null;
64
+
65
+ return {
66
+ sender: username,
67
+ nickname: user.nickname || '',
68
+ ip: user.ip || '',
69
+ status: user.status,
70
+ lastSeenAt: user.lastSeenAt || null,
71
+ onlineSince: user.onlineSince || null
72
+ };
30
73
  }
31
74
 
32
- setNickname(username, nickname) {
33
- const user = this.users.get(username);
75
+ setNickname({ roomId = DEFAULT_ROOM_ID, username, nickname = '' }) {
76
+ const user = this.getUser(roomId, username);
34
77
 
35
78
  if (!user) {
36
79
  return false;
@@ -40,36 +83,85 @@ class RoomManager {
40
83
  return true;
41
84
  }
42
85
 
43
- setOffline(username) {
44
- const user = this.users.get(username);
86
+ removeSocket({ roomId = DEFAULT_ROOM_ID, username, socketId }) {
87
+ const user = this.getUser(roomId, username);
45
88
 
46
89
  if (!user) {
47
- return false;
90
+ return { user: null, hasConnections: false };
91
+ }
92
+
93
+ user.socketIds.delete(socketId);
94
+ return {
95
+ user: this.getPublicUser(roomId, username),
96
+ hasConnections: user.socketIds.size > 0
97
+ };
98
+ }
99
+
100
+ markOffline({ roomId = DEFAULT_ROOM_ID, username }) {
101
+ const user = this.getUser(roomId, username);
102
+
103
+ if (!user) {
104
+ return { user: null, becameOffline: false };
48
105
  }
49
106
 
107
+ const becameOffline = user.status !== OFFLINE;
108
+ user.socketIds.clear();
50
109
  user.status = OFFLINE;
110
+ user.onlineSince = null;
111
+ user.lastSeenAt = Date.now();
112
+
113
+ return {
114
+ user: this.getPublicUser(roomId, username),
115
+ becameOffline
116
+ };
117
+ }
118
+
119
+ setOffline({ roomId = DEFAULT_ROOM_ID, username, socketId = null }) {
120
+ const user = this.getUser(roomId, username);
121
+
122
+ if (!user) {
123
+ return { user: null, becameOffline: false, hasConnections: false };
124
+ }
125
+
126
+ if (socketId) user.socketIds.delete(socketId);
127
+ const hasConnections = user.socketIds.size > 0;
128
+
129
+ return hasConnections
130
+ ? { user: this.getPublicUser(roomId, username), becameOffline: false, hasConnections }
131
+ : { ...this.markOffline({ roomId, username }), hasConnections: false };
132
+ }
133
+
134
+ listUsers(roomId = DEFAULT_ROOM_ID) {
135
+ const room = this.ensureRoom(roomId);
136
+ return [...room.users.keys()].map((username) => this.getPublicUser(roomId, username));
137
+ }
138
+
139
+ removeRoom(roomId = DEFAULT_ROOM_ID) {
140
+ if (roomId === DEFAULT_ROOM_ID) return false;
141
+ this.rooms.delete(roomId);
142
+ this.roomNames.delete(roomId);
51
143
  return true;
52
144
  }
53
145
 
54
- listUsers() {
55
- return [...this.users.entries()].map(([username, user]) => ({
56
- sender: username,
57
- nickname: user.nickname || '',
58
- ip: user.ip || '',
59
- status: user.status
60
- }));
146
+ setRoomName(roomId = DEFAULT_ROOM_ID, name = '') {
147
+ this.roomNames.set(roomId, name || '');
148
+ }
149
+
150
+ getRoomName(roomId = DEFAULT_ROOM_ID) {
151
+ return this.roomNames.get(roomId) || '';
61
152
  }
62
153
 
63
154
  setGroupName(groupName) {
64
- this.groupName = groupName || '';
155
+ this.setRoomName(DEFAULT_ROOM_ID, groupName);
65
156
  }
66
157
 
67
158
  getGroupName() {
68
- return this.groupName;
159
+ return this.getRoomName(DEFAULT_ROOM_ID);
69
160
  }
70
161
  }
71
162
 
72
163
  module.exports = {
73
164
  RoomManager,
74
- USER_STATUS: [ONLINE, OFFLINE]
165
+ USER_STATUS: [ONLINE, OFFLINE],
166
+ DEFAULT_ROOM_ID
75
167
  };
@@ -37,7 +37,11 @@ router.get('/getIndexInfo', (req, res) => {
37
37
 
38
38
  router.get('/getIndexInfo2', (req, res) => {
39
39
  const homeUrl = getRequestBaseUrl(req);
40
- const loginUrl = `${homeUrl}/login?receiver=${req.query.receiver || 'group_chat'}`;
40
+ const roomId = req.query.roomId;
41
+ const roomCode = req.query.code;
42
+ const loginUrl = roomId && roomId !== 'group_chat'
43
+ ? `${homeUrl}/login?roomId=${encodeURIComponent(roomId)}&code=${encodeURIComponent(roomCode || '')}`
44
+ : `${homeUrl}/login?receiver=${req.query.receiver || 'group_chat'}`;
41
45
 
42
46
  res.send({
43
47
  code: 0,
@@ -51,7 +55,11 @@ router.get('/getIndexInfo2', (req, res) => {
51
55
  router.get('/scanLogin', (req, res) => {
52
56
  const homeUrl = getRequestBaseUrl(req);
53
57
  const receiver = req.query.receiver || 'group_chat';
54
- const targetUrl = `${homeUrl}/chat?sender=${req.query.sender}&receiver=${receiver}`;
58
+ const roomId = req.query.roomId;
59
+ const roomCode = req.query.code;
60
+ const targetUrl = roomId && roomId !== 'group_chat'
61
+ ? `${homeUrl}/chat?sender=${req.query.sender}&roomId=${encodeURIComponent(roomId)}&code=${encodeURIComponent(roomCode || '')}`
62
+ : `${homeUrl}/chat?sender=${req.query.sender}&receiver=${receiver}`;
55
63
 
56
64
  res.send({
57
65
  code: 0,
@@ -60,9 +60,10 @@ class BatchDownloadService {
60
60
  this.tokens = new Map();
61
61
  }
62
62
 
63
- async createDownloadToken({ requester, chatType, targetUser, messageIds }) {
63
+ async createDownloadToken({ requester, roomId, chatType, targetUser, messageIds }) {
64
64
  const attachments = await this.historyService.getBatchAttachments({
65
65
  requester,
66
+ roomId,
66
67
  chatType,
67
68
  targetUser,
68
69
  messageIds
@@ -71,6 +72,7 @@ class BatchDownloadService {
71
72
  const token = crypto.randomBytes(24).toString('hex');
72
73
  this.tokens.set(token, {
73
74
  attachments,
75
+ roomId: roomId || null,
74
76
  expiresAt: Date.now() + DOWNLOAD_TOKEN_TTL
75
77
  });
76
78
  this.cleanupTokens();
@@ -104,6 +106,18 @@ class BatchDownloadService {
104
106
  }
105
107
  }
106
108
 
109
+ revokeRoom(roomId) {
110
+ if (!roomId) return 0;
111
+
112
+ let revoked = 0;
113
+ for (const [token, record] of this.tokens.entries()) {
114
+ if (record.roomId !== roomId) continue;
115
+ this.tokens.delete(token);
116
+ revoked += 1;
117
+ }
118
+ return revoked;
119
+ }
120
+
107
121
  streamArchive(res, record) {
108
122
  const usedNames = new Set();
109
123
  const availableAttachments = record.attachments