chattransfer 2.0.17 → 2.0.18
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-CiFpIcIA.js} +1 -1
- package/frontend/dist/assets/index-DDcqIagG.css +1 -0
- package/frontend/dist/assets/{index-B2dOrakb.js → index-PrmIL31V.js} +35 -35
- package/frontend/dist/index.html +2 -2
- package/package.json +4 -3
- package/server/index.js +6 -4
- package/server/routes/batch-download.js +31 -0
- package/server/routes/index.js +14 -2
- package/server/services/batch-download-service.js +161 -0
- package/server/services/message-history-service.js +102 -0
- package/server/socket/index.js +65 -1
- package/frontend/dist/assets/index-DYJPnabS.css +0 -1
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-PrmIL31V.js"></script>
|
|
9
|
+
<link rel="stylesheet" crossorigin href="/assets/index-DDcqIagG.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.
|
|
3
|
+
"version": "2.0.18",
|
|
4
4
|
"description": "LAN instant messaging and file transfer tool",
|
|
5
5
|
"main": "server/index.js",
|
|
6
6
|
"engines": {
|
|
@@ -32,10 +32,11 @@
|
|
|
32
32
|
"node-machine-id": "^1.1.12",
|
|
33
33
|
"prebuild-install": "7.1.3",
|
|
34
34
|
"qr-image": "^3.2.0",
|
|
35
|
-
"sqlite3": "5.1.7",
|
|
36
35
|
"socket.io": "^4.8.3",
|
|
36
|
+
"sqlite3": "5.1.7",
|
|
37
37
|
"underscore": "^1.10.2",
|
|
38
|
-
"urlencode": "^1.1.0"
|
|
38
|
+
"urlencode": "^1.1.0",
|
|
39
|
+
"yazl": "^3.3.1"
|
|
39
40
|
},
|
|
40
41
|
"bin": {
|
|
41
42
|
"chattransfer": "bin/chattransfer.js"
|
package/server/index.js
CHANGED
|
@@ -7,6 +7,7 @@ const { getCachePath, getConfigFile, getDefaultCachePath } = require('./config')
|
|
|
7
7
|
const registerRoutes = require('./routes/index');
|
|
8
8
|
const registerSocket = require('./socket/index');
|
|
9
9
|
const { MessageHistoryService } = require('./services/message-history-service');
|
|
10
|
+
const { BatchDownloadService } = require('./services/batch-download-service');
|
|
10
11
|
const { trackEvent } = require('./telemetry/compat');
|
|
11
12
|
const {
|
|
12
13
|
getDatabaseFile,
|
|
@@ -26,7 +27,7 @@ function createApp() {
|
|
|
26
27
|
return app;
|
|
27
28
|
}
|
|
28
29
|
|
|
29
|
-
function createConfiguredApp(cachePath) {
|
|
30
|
+
function createConfiguredApp(cachePath, historyService = null, batchDownloadService = null) {
|
|
30
31
|
const app = createApp();
|
|
31
32
|
const safeCachePath = cachePath
|
|
32
33
|
? { path: path.resolve(cachePath), changed: false }
|
|
@@ -43,7 +44,7 @@ function createConfiguredApp(cachePath) {
|
|
|
43
44
|
console.warn(`[Cache] fallback to default: ${safeCachePath.path}`);
|
|
44
45
|
}
|
|
45
46
|
|
|
46
|
-
registerRoutes(app, safeCachePath.path);
|
|
47
|
+
registerRoutes(app, safeCachePath.path, historyService, batchDownloadService);
|
|
47
48
|
return app;
|
|
48
49
|
}
|
|
49
50
|
|
|
@@ -115,6 +116,7 @@ async function start(options = {}) {
|
|
|
115
116
|
cachePath: uploadsDir,
|
|
116
117
|
databasePath: usesIndependentDatabaseFile() ? getDatabaseFile() : undefined
|
|
117
118
|
});
|
|
119
|
+
const batchDownloadService = new BatchDownloadService({ historyService });
|
|
118
120
|
|
|
119
121
|
try {
|
|
120
122
|
await historyService.init();
|
|
@@ -123,10 +125,10 @@ async function start(options = {}) {
|
|
|
123
125
|
return;
|
|
124
126
|
}
|
|
125
127
|
|
|
126
|
-
const app = createConfiguredApp(uploadsDir);
|
|
128
|
+
const app = createConfiguredApp(uploadsDir, historyService, batchDownloadService);
|
|
127
129
|
const server = http.createServer(app);
|
|
128
130
|
|
|
129
|
-
registerSocket(server, historyService);
|
|
131
|
+
registerSocket(server, historyService, batchDownloadService);
|
|
130
132
|
|
|
131
133
|
await new Promise((resolve, reject) => {
|
|
132
134
|
server.once('error', reject);
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const express = require('express');
|
|
2
|
+
|
|
3
|
+
module.exports = function registerBatchDownloadRoutes(batchDownloadService) {
|
|
4
|
+
if (!batchDownloadService) {
|
|
5
|
+
throw new Error('batchDownloadService is required');
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
const router = express.Router();
|
|
9
|
+
|
|
10
|
+
router.get('/batch-download/:token', (req, res) => {
|
|
11
|
+
const record = batchDownloadService.consumeDownloadToken(req.params.token);
|
|
12
|
+
|
|
13
|
+
if (!record) {
|
|
14
|
+
return res.status(410).send({
|
|
15
|
+
code: -1,
|
|
16
|
+
message: 'Download link expired or already used'
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
if (!batchDownloadService.streamArchive(res, record)) {
|
|
21
|
+
return res.status(404).send({
|
|
22
|
+
code: -1,
|
|
23
|
+
message: 'No downloadable attachment files'
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return undefined;
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
return router;
|
|
31
|
+
};
|
package/server/routes/index.js
CHANGED
|
@@ -4,11 +4,20 @@ const { trackEvent } = require('../telemetry/compat');
|
|
|
4
4
|
const fs = require('fs');
|
|
5
5
|
const fileUpload = require('express-fileupload');
|
|
6
6
|
const { getCachePath } = require('../config');
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
const { BatchDownloadService } = require('../services/batch-download-service');
|
|
8
|
+
|
|
9
|
+
module.exports = function registerRoutes(
|
|
10
|
+
app,
|
|
11
|
+
configuredUploadsDir = getCachePath(),
|
|
12
|
+
historyService = null,
|
|
13
|
+
batchDownloadService = null
|
|
14
|
+
) {
|
|
9
15
|
const uploadsDir = path.resolve(configuredUploadsDir);
|
|
10
16
|
const filesDir = path.join(uploadsDir, 'files');
|
|
11
17
|
const tempDir = path.join(uploadsDir, 'tmp');
|
|
18
|
+
const sharedBatchDownloadService = batchDownloadService || (
|
|
19
|
+
historyService ? new BatchDownloadService({ historyService }) : null
|
|
20
|
+
);
|
|
12
21
|
|
|
13
22
|
if (!fs.existsSync(filesDir)) {
|
|
14
23
|
fs.mkdirSync(filesDir, { recursive: true });
|
|
@@ -31,6 +40,9 @@ module.exports = function registerRoutes(app, configuredUploadsDir = getCachePat
|
|
|
31
40
|
app.use(require('./session'));
|
|
32
41
|
app.use(require('./qr'));
|
|
33
42
|
app.use(require('./files')(uploadsDir));
|
|
43
|
+
if (sharedBatchDownloadService) {
|
|
44
|
+
app.use(require('./batch-download')(sharedBatchDownloadService));
|
|
45
|
+
}
|
|
34
46
|
|
|
35
47
|
app.use(express.static(path.join(__dirname, '../../frontend/dist')));
|
|
36
48
|
app.use('/files', express.static(filesDir));
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const yazl = require('yazl');
|
|
5
|
+
|
|
6
|
+
const DOWNLOAD_TOKEN_TTL = 10 * 60 * 1000;
|
|
7
|
+
|
|
8
|
+
function sanitizeZipEntryName(name) {
|
|
9
|
+
const safeName = String(name || 'download')
|
|
10
|
+
.replace(/[\u0000-\u001f\u007f]/g, '')
|
|
11
|
+
.replace(/[\\/:*?"<>|]/g, '_')
|
|
12
|
+
.replace(/^\.+/, '_')
|
|
13
|
+
.trim();
|
|
14
|
+
|
|
15
|
+
return safeName || 'download';
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function buildUniqueZipEntryName(fileName, usedNames) {
|
|
19
|
+
const baseName = sanitizeZipEntryName(fileName);
|
|
20
|
+
const extension = path.extname(baseName);
|
|
21
|
+
const nameWithoutExtension = extension ? baseName.slice(0, -extension.length) : baseName;
|
|
22
|
+
let candidate = baseName;
|
|
23
|
+
let index = 1;
|
|
24
|
+
|
|
25
|
+
while (usedNames.has(candidate.toLowerCase())) {
|
|
26
|
+
index += 1;
|
|
27
|
+
candidate = `${nameWithoutExtension} (${index})${extension}`;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
usedNames.add(candidate.toLowerCase());
|
|
31
|
+
return candidate;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function buildManifest(attachments, missingAttachments = []) {
|
|
35
|
+
const lines = ['ChatTransfer batch download manifest', ''];
|
|
36
|
+
|
|
37
|
+
attachments.forEach((attachment, index) => {
|
|
38
|
+
lines.push(`${index + 1}. ${attachment.zipEntryName}`);
|
|
39
|
+
lines.push(` Message ID: ${attachment.msgId}`);
|
|
40
|
+
lines.push(` Time: ${attachment.createTime}`);
|
|
41
|
+
lines.push(` Sender: ${attachment.senderNickname || attachment.sender}`);
|
|
42
|
+
lines.push(` Size: ${Number(attachment.fileSize) || 0} bytes`);
|
|
43
|
+
lines.push('');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
if (missingAttachments.length > 0) {
|
|
47
|
+
lines.push('Missing files:', '');
|
|
48
|
+
missingAttachments.forEach((attachment) => {
|
|
49
|
+
lines.push(`- ${attachment.fileName || attachment.msgId}`);
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return lines.join('\n');
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
class BatchDownloadService {
|
|
57
|
+
constructor({ historyService }) {
|
|
58
|
+
if (!historyService) throw new Error('historyService is required');
|
|
59
|
+
this.historyService = historyService;
|
|
60
|
+
this.tokens = new Map();
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async createDownloadToken({ requester, chatType, targetUser, messageIds }) {
|
|
64
|
+
const attachments = await this.historyService.getBatchAttachments({
|
|
65
|
+
requester,
|
|
66
|
+
chatType,
|
|
67
|
+
targetUser,
|
|
68
|
+
messageIds
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
const token = crypto.randomBytes(24).toString('hex');
|
|
72
|
+
this.tokens.set(token, {
|
|
73
|
+
attachments,
|
|
74
|
+
expiresAt: Date.now() + DOWNLOAD_TOKEN_TTL
|
|
75
|
+
});
|
|
76
|
+
this.cleanupTokens();
|
|
77
|
+
|
|
78
|
+
return {
|
|
79
|
+
token,
|
|
80
|
+
downloadUrl: `/batch-download/${token}`,
|
|
81
|
+
expiresIn: DOWNLOAD_TOKEN_TTL
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
consumeDownloadToken(token) {
|
|
86
|
+
if (typeof token !== 'string') return null;
|
|
87
|
+
|
|
88
|
+
const record = this.tokens.get(token);
|
|
89
|
+
if (!record) return null;
|
|
90
|
+
this.tokens.delete(token);
|
|
91
|
+
|
|
92
|
+
if (Date.now() > record.expiresAt) {
|
|
93
|
+
this.cleanupTokens();
|
|
94
|
+
return null;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return record;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
cleanupTokens() {
|
|
101
|
+
const now = Date.now();
|
|
102
|
+
for (const [token, record] of this.tokens.entries()) {
|
|
103
|
+
if (now > record.expiresAt) this.tokens.delete(token);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
streamArchive(res, record) {
|
|
108
|
+
const usedNames = new Set();
|
|
109
|
+
const availableAttachments = record.attachments
|
|
110
|
+
.map((attachment) => ({
|
|
111
|
+
...attachment,
|
|
112
|
+
zipEntryName: buildUniqueZipEntryName(attachment.fileName, usedNames)
|
|
113
|
+
}))
|
|
114
|
+
.filter((attachment) => {
|
|
115
|
+
try {
|
|
116
|
+
return fs.statSync(attachment.physicalPath).isFile();
|
|
117
|
+
} catch (error) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const missingAttachments = record.attachments.filter((attachment) => (
|
|
123
|
+
!availableAttachments.some((item) => item.msgId === attachment.msgId)
|
|
124
|
+
));
|
|
125
|
+
|
|
126
|
+
if (availableAttachments.length === 0) {
|
|
127
|
+
return false;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const archive = new yazl.ZipFile();
|
|
131
|
+
const zipFileName = `ChatTransfer-${new Date().toISOString().replace(/[:.]/g, '-')}.zip`;
|
|
132
|
+
res.status(200);
|
|
133
|
+
res.setHeader('Content-Type', 'application/zip');
|
|
134
|
+
res.setHeader('Content-Disposition', `attachment; filename="${zipFileName}"`);
|
|
135
|
+
archive.outputStream.pipe(res);
|
|
136
|
+
archive.outputStream.once('error', (error) => {
|
|
137
|
+
console.error('[Batch download] archive failed:', error.message);
|
|
138
|
+
res.destroy(error);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
availableAttachments.forEach((attachment) => {
|
|
142
|
+
const isAlreadyCompressed = /^(image|video)\//.test(attachment.fileType || '');
|
|
143
|
+
archive.addFile(attachment.physicalPath, attachment.zipEntryName, {
|
|
144
|
+
compress: !isAlreadyCompressed
|
|
145
|
+
});
|
|
146
|
+
});
|
|
147
|
+
archive.addBuffer(
|
|
148
|
+
Buffer.from(buildManifest(availableAttachments, missingAttachments), 'utf8'),
|
|
149
|
+
'manifest.txt'
|
|
150
|
+
);
|
|
151
|
+
archive.end();
|
|
152
|
+
|
|
153
|
+
return true;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
module.exports = {
|
|
158
|
+
BatchDownloadService,
|
|
159
|
+
buildUniqueZipEntryName,
|
|
160
|
+
sanitizeZipEntryName
|
|
161
|
+
};
|
|
@@ -5,6 +5,8 @@ const sqlite3 = require('sqlite3');
|
|
|
5
5
|
|
|
6
6
|
const DEFAULT_LIMIT = 100;
|
|
7
7
|
const MAX_LIMIT = 200;
|
|
8
|
+
const MAX_BATCH_ATTACHMENT_COUNT = 200;
|
|
9
|
+
const MAX_BATCH_ATTACHMENT_TOTAL_SIZE = 2 * 1024 * 1024 * 1024;
|
|
8
10
|
const GROUP_SESSION_ID = 'group';
|
|
9
11
|
|
|
10
12
|
class MessageHistoryService {
|
|
@@ -147,6 +149,82 @@ class MessageHistoryService {
|
|
|
147
149
|
};
|
|
148
150
|
}
|
|
149
151
|
|
|
152
|
+
async queryAttachments({ requester, chatType, targetUser }) {
|
|
153
|
+
this.ensureOpened();
|
|
154
|
+
|
|
155
|
+
const conversation = this.getConversation({ requester, chatType, targetUser });
|
|
156
|
+
const rows = await this.all(
|
|
157
|
+
`SELECT seq, id, sender_id, sender_nickname, message_type, create_time, created_at,
|
|
158
|
+
file_path, file_name, file_size, file_mime_type
|
|
159
|
+
FROM messages
|
|
160
|
+
WHERE session_id = ? AND file_path IS NOT NULL
|
|
161
|
+
ORDER BY created_at ASC, seq ASC`,
|
|
162
|
+
[conversation.sessionId]
|
|
163
|
+
);
|
|
164
|
+
|
|
165
|
+
const attachments = [];
|
|
166
|
+
for (const row of rows) {
|
|
167
|
+
const physicalPath = this.resolveExistingFilePath(row.file_path);
|
|
168
|
+
if (!physicalPath) continue;
|
|
169
|
+
const stats = fs.statSync(physicalPath);
|
|
170
|
+
if (!stats.isFile()) continue;
|
|
171
|
+
const physicalFileName = path.posix.basename(row.file_path);
|
|
172
|
+
|
|
173
|
+
attachments.push({
|
|
174
|
+
msgId: row.id,
|
|
175
|
+
seq: row.seq,
|
|
176
|
+
sender: row.sender_id,
|
|
177
|
+
senderNickname: row.sender_nickname || row.sender_id,
|
|
178
|
+
msgType: row.message_type,
|
|
179
|
+
createTime: row.create_time,
|
|
180
|
+
createdAt: row.created_at,
|
|
181
|
+
fileName: row.file_name || physicalFileName,
|
|
182
|
+
fileSize: Number(row.file_size) || stats.size,
|
|
183
|
+
fileType: row.file_mime_type,
|
|
184
|
+
fileUrl: `/files/${encodeURIComponent(physicalFileName)}`,
|
|
185
|
+
filePath: row.file_path,
|
|
186
|
+
physicalPath,
|
|
187
|
+
available: true
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return { attachments };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async getBatchAttachments({ requester, chatType, targetUser, messageIds }) {
|
|
195
|
+
this.ensureOpened();
|
|
196
|
+
|
|
197
|
+
if (!Array.isArray(messageIds) || messageIds.length === 0) {
|
|
198
|
+
throw new Error('No attachment selected');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const requestedIds = new Set(messageIds.filter((id) => typeof id === 'string' && id.length > 0));
|
|
202
|
+
if (requestedIds.size === 0) throw new Error('No attachment selected');
|
|
203
|
+
if (requestedIds.size > MAX_BATCH_ATTACHMENT_COUNT) {
|
|
204
|
+
throw new Error(`Batch download supports at most ${MAX_BATCH_ATTACHMENT_COUNT} files`);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const { attachments } = await this.queryAttachments({ requester, chatType, targetUser });
|
|
208
|
+
const attachmentMap = new Map(attachments.map((attachment) => [attachment.msgId, attachment]));
|
|
209
|
+
const selectedAttachments = [];
|
|
210
|
+
|
|
211
|
+
for (const messageId of requestedIds) {
|
|
212
|
+
const attachment = attachmentMap.get(messageId);
|
|
213
|
+
if (!attachment) throw new Error(`Attachment not found: ${messageId}`);
|
|
214
|
+
if (!attachment.available) throw new Error(`Attachment file missing: ${attachment.fileName}`);
|
|
215
|
+
selectedAttachments.push(attachment);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const totalSize = selectedAttachments.reduce((total, attachment) => (
|
|
219
|
+
total + (Number(attachment.fileSize) || 0)
|
|
220
|
+
), 0);
|
|
221
|
+
if (totalSize > MAX_BATCH_ATTACHMENT_TOTAL_SIZE) {
|
|
222
|
+
throw new Error('Batch download exceeds the 2GB size limit');
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
return selectedAttachments;
|
|
226
|
+
}
|
|
227
|
+
|
|
150
228
|
clearHistory({ requester, chatType, targetUser }) {
|
|
151
229
|
this.ensureOpened();
|
|
152
230
|
|
|
@@ -411,6 +489,30 @@ class MessageHistoryService {
|
|
|
411
489
|
return physicalPath;
|
|
412
490
|
}
|
|
413
491
|
|
|
492
|
+
resolveExistingFilePath(filePath) {
|
|
493
|
+
const physicalPath = this.resolveFilePath(filePath);
|
|
494
|
+
if (!physicalPath) return null;
|
|
495
|
+
|
|
496
|
+
try {
|
|
497
|
+
const realCachePath = fs.realpathSync(this.cachePath);
|
|
498
|
+
const realFilesPath = path.join(realCachePath, 'files');
|
|
499
|
+
const realPhysicalPath = fs.realpathSync(physicalPath);
|
|
500
|
+
const relativePath = path.relative(realFilesPath, realPhysicalPath);
|
|
501
|
+
|
|
502
|
+
if (
|
|
503
|
+
relativePath === '' ||
|
|
504
|
+
relativePath.startsWith('..') ||
|
|
505
|
+
path.isAbsolute(relativePath)
|
|
506
|
+
) {
|
|
507
|
+
return null;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return realPhysicalPath;
|
|
511
|
+
} catch (error) {
|
|
512
|
+
return null;
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
414
516
|
isValidUserId(value) {
|
|
415
517
|
return typeof value === 'string' && value.trim().length > 0 && value.length <= 256;
|
|
416
518
|
}
|
package/server/socket/index.js
CHANGED
|
@@ -6,10 +6,13 @@ const {
|
|
|
6
6
|
decoratePrivateMessage
|
|
7
7
|
} = require('../services/message-service');
|
|
8
8
|
|
|
9
|
-
module.exports = function registerSocket(server, historyService) {
|
|
9
|
+
module.exports = function registerSocket(server, historyService, batchDownloadService) {
|
|
10
10
|
if (!historyService) {
|
|
11
11
|
throw new Error('historyService is required');
|
|
12
12
|
}
|
|
13
|
+
if (!batchDownloadService) {
|
|
14
|
+
throw new Error('batchDownloadService is required');
|
|
15
|
+
}
|
|
13
16
|
|
|
14
17
|
const io = socketIo(server, {
|
|
15
18
|
cors: {
|
|
@@ -190,6 +193,67 @@ module.exports = function registerSocket(server, historyService) {
|
|
|
190
193
|
}
|
|
191
194
|
});
|
|
192
195
|
|
|
196
|
+
socket.on('attachment_query', async (params = {}, ack) => {
|
|
197
|
+
if (typeof ack !== 'function') return;
|
|
198
|
+
|
|
199
|
+
try {
|
|
200
|
+
const requester = socket.username || params.requester;
|
|
201
|
+
const chatType = params.chatType || (
|
|
202
|
+
params.targetUser === 'group_chat' ? 'group' : 'private'
|
|
203
|
+
);
|
|
204
|
+
const { attachments } = await historyService.queryAttachments({
|
|
205
|
+
requester,
|
|
206
|
+
chatType,
|
|
207
|
+
targetUser: params.targetUser
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
ack({
|
|
211
|
+
code: 0,
|
|
212
|
+
data: {
|
|
213
|
+
attachments: attachments.map((attachment) => ({
|
|
214
|
+
msgId: attachment.msgId,
|
|
215
|
+
seq: attachment.seq,
|
|
216
|
+
sender: attachment.sender,
|
|
217
|
+
senderNickname: attachment.senderNickname,
|
|
218
|
+
msgType: attachment.msgType,
|
|
219
|
+
createTime: attachment.createTime,
|
|
220
|
+
createdAt: attachment.createdAt,
|
|
221
|
+
fileName: attachment.fileName,
|
|
222
|
+
fileSize: attachment.fileSize,
|
|
223
|
+
fileType: attachment.fileType,
|
|
224
|
+
fileUrl: attachment.fileUrl,
|
|
225
|
+
available: attachment.available
|
|
226
|
+
}))
|
|
227
|
+
}
|
|
228
|
+
});
|
|
229
|
+
} catch (error) {
|
|
230
|
+
console.error('[Attachment] query failed:', error.message);
|
|
231
|
+
ack({ code: -1, message: 'Failed to load attachments' });
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
|
|
235
|
+
socket.on('attachment_download', async (params = {}, ack) => {
|
|
236
|
+
if (typeof ack !== 'function') return;
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
const requester = socket.username || params.requester;
|
|
240
|
+
const chatType = params.chatType || (
|
|
241
|
+
params.targetUser === 'group_chat' ? 'group' : 'private'
|
|
242
|
+
);
|
|
243
|
+
const data = await batchDownloadService.createDownloadToken({
|
|
244
|
+
requester,
|
|
245
|
+
chatType,
|
|
246
|
+
targetUser: params.targetUser,
|
|
247
|
+
messageIds: params.messageIds
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
ack({ code: 0, data });
|
|
251
|
+
} catch (error) {
|
|
252
|
+
console.error('[Attachment] download request failed:', error.message);
|
|
253
|
+
ack({ code: -1, message: error.message });
|
|
254
|
+
}
|
|
255
|
+
});
|
|
256
|
+
|
|
193
257
|
socket.on('history_clear', async (params = {}, ack) => {
|
|
194
258
|
if (typeof ack !== 'function') return;
|
|
195
259
|
|