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.
- package/frontend/dist/assets/{browser-CiFpIcIA.js → browser-_rMkfdfG.js} +1 -1
- package/frontend/dist/assets/{index-PrmIL31V.js → index-BTJ_Fb6p.js} +42 -42
- package/frontend/dist/assets/{index-DDcqIagG.css → index-IvWY8A4p.css} +1 -1
- package/frontend/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/cli-options.js +9 -1
- package/server/index.js +15 -1
- package/server/room-manager.js +117 -25
- package/server/routes/qr.js +10 -2
- package/server/services/batch-download-service.js +15 -1
- package/server/services/message-history-service.js +684 -20
- package/server/socket/index.js +536 -59
- package/server/utils/network.js +11 -1
- package/server/utils/runtime-options.js +30 -0
package/frontend/dist/index.html
CHANGED
|
@@ -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-
|
|
9
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
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
package/server/cli-options.js
CHANGED
|
@@ -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,
|
package/server/room-manager.js
CHANGED
|
@@ -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.
|
|
7
|
-
this.
|
|
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
|
|
20
|
-
|
|
21
|
-
|
|
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.
|
|
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
|
-
|
|
44
|
-
const user = this.
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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.
|
|
155
|
+
this.setRoomName(DEFAULT_ROOM_ID, groupName);
|
|
65
156
|
}
|
|
66
157
|
|
|
67
158
|
getGroupName() {
|
|
68
|
-
return this.
|
|
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
|
};
|
package/server/routes/qr.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|