chattransfer 0.0.0-stage → 2.0.10
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/LICENSE +21 -0
- package/README.ar.md +208 -0
- package/README.md +207 -2
- package/README.zh-CN.md +208 -0
- package/bin/chattransfer.js +38 -0
- package/frontend/README.md +5 -0
- package/frontend/dist/assets/browser-BqOeEQBb.js +8 -0
- package/frontend/dist/assets/index-DTsuxum5.js +976 -0
- package/frontend/dist/assets/index-Dw5X-Gok.css +1 -0
- package/frontend/dist/assets/logo-DQUxWwh1.png +0 -0
- package/frontend/dist/index.html +14 -0
- package/frontend/dist/pin.svg +1 -0
- package/frontend/dist/vite.svg +1 -0
- package/package.json +62 -4
- package/server/cli-options.js +69 -0
- package/server/config/loader.js +36 -0
- package/server/config.js +63 -0
- package/server/index.js +132 -0
- package/server/room-manager.js +75 -0
- package/server/routes/checkLogin.js +24 -0
- package/server/routes/files.js +49 -0
- package/server/routes/geneBUserQR.js +37 -0
- package/server/routes/geneQR.js +39 -0
- package/server/routes/getHomeDir.js +27 -0
- package/server/routes/getIndexInfo.js +42 -0
- package/server/routes/getIndexInfo2.js +42 -0
- package/server/routes/index.js +36 -0
- package/server/routes/openCachePath.js +63 -0
- package/server/routes/qr.js +63 -0
- package/server/routes/scanLogin.js +33 -0
- package/server/routes/session.js +15 -0
- package/server/routes/setCachePath.js +94 -0
- package/server/routes/system.js +107 -0
- package/server/services/message-history-service.js +476 -0
- package/server/services/message-service.js +31 -0
- package/server/socket/index.js +232 -0
- package/server/socket/session-manager.js +3 -0
- package/server/telemetry/amplitude-provider.js +17 -0
- package/server/telemetry/compat.js +17 -0
- package/server/telemetry/index.js +26 -0
- package/server/telemetry/noop-provider.js +7 -0
- package/server/tool.js +71 -0
- package/server/utils/app-root.js +11 -0
- package/server/utils/network.js +21 -0
- package/server/utils/request-url.js +15 -0
- package/server/utils/runtime-options.js +88 -0
- package/server/utils/runtime-paths.js +87 -0
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
const crypto = require('crypto');
|
|
2
|
+
const fs = require('fs');
|
|
3
|
+
const path = require('path');
|
|
4
|
+
const sqlite3 = require('sqlite3');
|
|
5
|
+
|
|
6
|
+
const DEFAULT_LIMIT = 100;
|
|
7
|
+
const MAX_LIMIT = 200;
|
|
8
|
+
const GROUP_SESSION_ID = 'group';
|
|
9
|
+
|
|
10
|
+
class MessageHistoryService {
|
|
11
|
+
constructor({ cachePath, databasePath }) {
|
|
12
|
+
if (!cachePath) {
|
|
13
|
+
throw new Error('cachePath is required');
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
this.cachePath = path.resolve(cachePath);
|
|
17
|
+
this.databasePath = databasePath
|
|
18
|
+
? path.resolve(databasePath)
|
|
19
|
+
: path.join(this.cachePath, 'history', 'chattransfer.db');
|
|
20
|
+
this.historyDir = path.dirname(this.databasePath);
|
|
21
|
+
this.db = null;
|
|
22
|
+
this.writeLock = Promise.resolve();
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
init() {
|
|
26
|
+
return new Promise((resolve, reject) => {
|
|
27
|
+
fs.mkdir(this.historyDir, { recursive: true }, (error) => {
|
|
28
|
+
if (error) {
|
|
29
|
+
reject(error);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
this.db = new sqlite3.Database(this.databasePath, (openError) => {
|
|
34
|
+
if (openError) {
|
|
35
|
+
reject(openError);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
this.migrate()
|
|
40
|
+
.then(() => this.processCleanupQueue())
|
|
41
|
+
.then(resolve)
|
|
42
|
+
.catch(reject);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
close() {
|
|
49
|
+
if (!this.db) return Promise.resolve();
|
|
50
|
+
|
|
51
|
+
return new Promise((resolve, reject) => {
|
|
52
|
+
this.db.close((error) => (error ? reject(error) : resolve()));
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
saveMessage(message) {
|
|
57
|
+
this.ensureOpened();
|
|
58
|
+
|
|
59
|
+
return this.enqueueWrite(() => this.saveMessageInTransaction(message));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
async saveMessageInTransaction(message) {
|
|
63
|
+
|
|
64
|
+
const normalized = this.normalizeMessage(message);
|
|
65
|
+
const conversation = this.getConversation({
|
|
66
|
+
chatType: normalized.receiver === 'group_chat' ? 'group' : 'private',
|
|
67
|
+
requester: normalized.sender,
|
|
68
|
+
targetUser: normalized.receiver
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
await this.run('BEGIN IMMEDIATE');
|
|
72
|
+
|
|
73
|
+
try {
|
|
74
|
+
await this.run(
|
|
75
|
+
'INSERT OR IGNORE INTO sessions(id, chat_type, created_at) VALUES (?, ?, ?)',
|
|
76
|
+
[conversation.sessionId, conversation.chatType, normalized.createdAt]
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
for (const participant of conversation.participants) {
|
|
80
|
+
await this.run(
|
|
81
|
+
'INSERT OR IGNORE INTO session_participants(session_id, user_id) VALUES (?, ?)',
|
|
82
|
+
[conversation.sessionId, participant]
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await this.run(
|
|
87
|
+
`INSERT INTO messages(
|
|
88
|
+
id, session_id, sender_id, receiver_id, message_type, content,
|
|
89
|
+
sender_nickname, sender_ip, create_time, created_at,
|
|
90
|
+
file_path, file_name, file_size, file_mime_type
|
|
91
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
92
|
+
[
|
|
93
|
+
normalized.msgId,
|
|
94
|
+
conversation.sessionId,
|
|
95
|
+
normalized.sender,
|
|
96
|
+
normalized.receiver,
|
|
97
|
+
normalized.msgType,
|
|
98
|
+
normalized.content,
|
|
99
|
+
normalized.sender_nickname || null,
|
|
100
|
+
normalized.sender_ip || null,
|
|
101
|
+
normalized.createTime,
|
|
102
|
+
normalized.createdAt,
|
|
103
|
+
normalized.filePath || null,
|
|
104
|
+
normalized.fileName || null,
|
|
105
|
+
normalized.fileSize || null,
|
|
106
|
+
normalized.fileType || null
|
|
107
|
+
]
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
await this.run('COMMIT');
|
|
111
|
+
return normalized;
|
|
112
|
+
} catch (error) {
|
|
113
|
+
await this.run('ROLLBACK').catch(() => {});
|
|
114
|
+
throw error;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async queryHistory({ requester, chatType, targetUser, cursor = null, limit = DEFAULT_LIMIT }) {
|
|
119
|
+
this.ensureOpened();
|
|
120
|
+
|
|
121
|
+
const conversation = this.getConversation({ requester, chatType, targetUser });
|
|
122
|
+
const queryLimit = this.normalizeLimit(limit);
|
|
123
|
+
const normalizedCursor = this.normalizeCursor(cursor);
|
|
124
|
+
const direction = normalizedCursor === null ? 'DESC' : 'ASC';
|
|
125
|
+
const cursorCondition = normalizedCursor === null ? '' : 'AND seq > ?';
|
|
126
|
+
const params = normalizedCursor === null
|
|
127
|
+
? [conversation.sessionId, queryLimit + 1]
|
|
128
|
+
: [conversation.sessionId, normalizedCursor, queryLimit + 1];
|
|
129
|
+
|
|
130
|
+
const rows = await this.all(
|
|
131
|
+
`SELECT * FROM messages
|
|
132
|
+
WHERE session_id = ? ${cursorCondition}
|
|
133
|
+
ORDER BY seq ${direction}
|
|
134
|
+
LIMIT ?`,
|
|
135
|
+
params
|
|
136
|
+
);
|
|
137
|
+
const hasMore = rows.length > queryLimit;
|
|
138
|
+
const messages = (hasMore ? rows.slice(0, queryLimit) : rows)
|
|
139
|
+
.map((row) => this.mapMessage(row))
|
|
140
|
+
.sort((a, b) => a.seq - b.seq);
|
|
141
|
+
const nextCursor = messages.length > 0 ? messages[messages.length - 1].seq : normalizedCursor;
|
|
142
|
+
|
|
143
|
+
return {
|
|
144
|
+
messages,
|
|
145
|
+
hasMore,
|
|
146
|
+
nextCursor
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
clearHistory({ requester, chatType, targetUser }) {
|
|
151
|
+
this.ensureOpened();
|
|
152
|
+
|
|
153
|
+
return this.enqueueWrite(() => this.clearHistoryInTransaction({
|
|
154
|
+
requester,
|
|
155
|
+
chatType,
|
|
156
|
+
targetUser
|
|
157
|
+
}));
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
async clearHistoryInTransaction({ requester, chatType, targetUser }) {
|
|
161
|
+
|
|
162
|
+
const conversation = this.getConversation({ requester, chatType, targetUser });
|
|
163
|
+
const files = await this.all(
|
|
164
|
+
'SELECT DISTINCT file_path FROM messages WHERE session_id = ? AND file_path IS NOT NULL',
|
|
165
|
+
[conversation.sessionId]
|
|
166
|
+
);
|
|
167
|
+
|
|
168
|
+
await this.run('BEGIN IMMEDIATE');
|
|
169
|
+
|
|
170
|
+
try {
|
|
171
|
+
for (const file of files) {
|
|
172
|
+
await this.run(
|
|
173
|
+
'INSERT OR IGNORE INTO file_cleanup_queue(file_path, created_at) VALUES (?, ?)',
|
|
174
|
+
[file.file_path, Date.now()]
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
await this.run('DELETE FROM messages WHERE session_id = ?', [conversation.sessionId]);
|
|
179
|
+
await this.run('COMMIT');
|
|
180
|
+
} catch (error) {
|
|
181
|
+
await this.run('ROLLBACK').catch(() => {});
|
|
182
|
+
throw error;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const cleanup = await this.processCleanupQueue();
|
|
186
|
+
return {
|
|
187
|
+
sessionId: conversation.sessionId,
|
|
188
|
+
deletedFiles: cleanup.deletedFiles,
|
|
189
|
+
skippedFiles: cleanup.skippedFiles,
|
|
190
|
+
failedFiles: cleanup.failedFiles
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
async processCleanupQueue() {
|
|
195
|
+
this.ensureOpened();
|
|
196
|
+
|
|
197
|
+
const queue = await this.all('SELECT file_path FROM file_cleanup_queue');
|
|
198
|
+
let deletedFiles = 0;
|
|
199
|
+
let skippedFiles = 0;
|
|
200
|
+
let failedFiles = 0;
|
|
201
|
+
|
|
202
|
+
for (const item of queue) {
|
|
203
|
+
const references = await this.get(
|
|
204
|
+
'SELECT COUNT(*) AS count FROM messages WHERE file_path = ?',
|
|
205
|
+
[item.file_path]
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
if (references.count > 0) {
|
|
209
|
+
await this.run('DELETE FROM file_cleanup_queue WHERE file_path = ?', [item.file_path]);
|
|
210
|
+
skippedFiles += 1;
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const physicalPath = this.resolveFilePath(item.file_path);
|
|
215
|
+
|
|
216
|
+
if (!physicalPath) {
|
|
217
|
+
await this.updateCleanupFailure(item.file_path, 'Invalid file path');
|
|
218
|
+
failedFiles += 1;
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
try {
|
|
223
|
+
await fs.promises.unlink(physicalPath);
|
|
224
|
+
await this.run('DELETE FROM file_cleanup_queue WHERE file_path = ?', [item.file_path]);
|
|
225
|
+
deletedFiles += 1;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
if (error.code === 'ENOENT') {
|
|
228
|
+
await this.run('DELETE FROM file_cleanup_queue WHERE file_path = ?', [item.file_path]);
|
|
229
|
+
skippedFiles += 1;
|
|
230
|
+
} else {
|
|
231
|
+
await this.updateCleanupFailure(item.file_path, error.message);
|
|
232
|
+
failedFiles += 1;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
return { deletedFiles, skippedFiles, failedFiles };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async migrate() {
|
|
241
|
+
await this.run('PRAGMA foreign_keys = ON');
|
|
242
|
+
await this.run(`CREATE TABLE IF NOT EXISTS sessions (
|
|
243
|
+
id TEXT PRIMARY KEY,
|
|
244
|
+
chat_type TEXT NOT NULL,
|
|
245
|
+
created_at INTEGER NOT NULL
|
|
246
|
+
)`);
|
|
247
|
+
await this.run(`CREATE TABLE IF NOT EXISTS session_participants (
|
|
248
|
+
session_id TEXT NOT NULL,
|
|
249
|
+
user_id TEXT NOT NULL,
|
|
250
|
+
PRIMARY KEY (session_id, user_id),
|
|
251
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id)
|
|
252
|
+
)`);
|
|
253
|
+
await this.run(`CREATE TABLE IF NOT EXISTS messages (
|
|
254
|
+
seq INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
255
|
+
id TEXT NOT NULL UNIQUE,
|
|
256
|
+
session_id TEXT NOT NULL,
|
|
257
|
+
sender_id TEXT NOT NULL,
|
|
258
|
+
receiver_id TEXT NOT NULL,
|
|
259
|
+
message_type TEXT NOT NULL,
|
|
260
|
+
content TEXT,
|
|
261
|
+
sender_nickname TEXT,
|
|
262
|
+
sender_ip TEXT,
|
|
263
|
+
create_time TEXT NOT NULL,
|
|
264
|
+
created_at INTEGER NOT NULL,
|
|
265
|
+
file_path TEXT,
|
|
266
|
+
file_name TEXT,
|
|
267
|
+
file_size INTEGER,
|
|
268
|
+
file_mime_type TEXT,
|
|
269
|
+
FOREIGN KEY (session_id) REFERENCES sessions(id)
|
|
270
|
+
)`);
|
|
271
|
+
await this.run(`CREATE INDEX IF NOT EXISTS idx_messages_session_created_at
|
|
272
|
+
ON messages(session_id, created_at)`);
|
|
273
|
+
await this.run(`CREATE TABLE IF NOT EXISTS file_cleanup_queue (
|
|
274
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
275
|
+
file_path TEXT NOT NULL UNIQUE,
|
|
276
|
+
retry_count INTEGER NOT NULL DEFAULT 0,
|
|
277
|
+
last_error TEXT,
|
|
278
|
+
created_at INTEGER NOT NULL
|
|
279
|
+
)`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
getConversation({ requester, chatType, targetUser }) {
|
|
283
|
+
if (!this.isValidUserId(requester)) {
|
|
284
|
+
throw new Error('Invalid requester');
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const normalizedChatType = targetUser === 'group_chat' ? 'group' : chatType;
|
|
288
|
+
|
|
289
|
+
if (normalizedChatType === 'group') {
|
|
290
|
+
if (targetUser !== 'group_chat') {
|
|
291
|
+
throw new Error('Group history requires group_chat target');
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
return {
|
|
295
|
+
sessionId: GROUP_SESSION_ID,
|
|
296
|
+
chatType: 'group',
|
|
297
|
+
participants: []
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (normalizedChatType !== 'private') {
|
|
302
|
+
throw new Error('Invalid chat type');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!this.isValidUserId(targetUser) || targetUser === requester) {
|
|
306
|
+
throw new Error('Invalid private chat target');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const participants = [requester, targetUser].sort();
|
|
310
|
+
const hash = crypto.createHash('sha256').update(participants.join('\n')).digest('hex');
|
|
311
|
+
|
|
312
|
+
return {
|
|
313
|
+
sessionId: `private-${hash}`,
|
|
314
|
+
chatType: 'private',
|
|
315
|
+
participants
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
normalizeMessage(message) {
|
|
320
|
+
if (!this.isValidUserId(message.sender)) {
|
|
321
|
+
throw new Error('Invalid message sender');
|
|
322
|
+
}
|
|
323
|
+
if (!this.isValidUserId(message.receiver)) {
|
|
324
|
+
throw new Error('Invalid message receiver');
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const createdAt = Number(message.createdAt) || Date.now();
|
|
328
|
+
const filePath = this.extractFilePath(message);
|
|
329
|
+
|
|
330
|
+
return {
|
|
331
|
+
...message,
|
|
332
|
+
msgId: message.msgId || crypto.randomUUID(),
|
|
333
|
+
msgType: message.msgType || 'TEXT',
|
|
334
|
+
content: message.content ?? message.text ?? message.msg ?? '',
|
|
335
|
+
createTime: message.createTime || new Date(createdAt).toLocaleString(),
|
|
336
|
+
createdAt,
|
|
337
|
+
filePath,
|
|
338
|
+
fileName: message.fileName || null,
|
|
339
|
+
fileSize: Number(message.fileSize) || null,
|
|
340
|
+
fileType: message.fileType || message.mimetype || null
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
mapMessage(row) {
|
|
345
|
+
const message = {
|
|
346
|
+
seq: row.seq,
|
|
347
|
+
msgId: row.id,
|
|
348
|
+
sender: row.sender_id,
|
|
349
|
+
receiver: row.receiver_id,
|
|
350
|
+
msgType: row.message_type,
|
|
351
|
+
content: row.content || '',
|
|
352
|
+
text: row.content || '',
|
|
353
|
+
sender_nickname: row.sender_nickname || '',
|
|
354
|
+
sender_ip: row.sender_ip || '',
|
|
355
|
+
senderPhotoNickname: row.sender_id.slice(0, 2),
|
|
356
|
+
createTime: row.create_time,
|
|
357
|
+
createdAt: row.created_at,
|
|
358
|
+
status: 'success'
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
if (row.file_path) {
|
|
362
|
+
const fileName = row.file_name || path.posix.basename(row.file_path);
|
|
363
|
+
message.fileUrl = `/files/${encodeURIComponent(fileName)}`;
|
|
364
|
+
message.fileName = fileName;
|
|
365
|
+
message.fileSize = row.file_size;
|
|
366
|
+
message.fileType = row.file_mime_type;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
return message;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
extractFilePath(message) {
|
|
373
|
+
if (message.filePath) return this.normalizeStoredFilePath(message.filePath);
|
|
374
|
+
|
|
375
|
+
if (message.fileUrl) {
|
|
376
|
+
try {
|
|
377
|
+
const url = new URL(message.fileUrl, 'http://chattransfer.local');
|
|
378
|
+
const decodedPath = decodeURIComponent(url.pathname);
|
|
379
|
+
if (decodedPath.startsWith('/files/')) {
|
|
380
|
+
return this.normalizeStoredFilePath(decodedPath.slice(1));
|
|
381
|
+
}
|
|
382
|
+
} catch (error) {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
return null;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
normalizeStoredFilePath(filePath) {
|
|
391
|
+
if (typeof filePath !== 'string') return null;
|
|
392
|
+
|
|
393
|
+
const normalized = path.posix.normalize(filePath).replace(/^\/+/, '');
|
|
394
|
+
if (normalized.startsWith('..') || path.posix.isAbsolute(normalized)) {
|
|
395
|
+
return null;
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return normalized.startsWith('files/') ? normalized : `files/${path.posix.basename(normalized)}`;
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
resolveFilePath(filePath) {
|
|
402
|
+
const normalized = this.normalizeStoredFilePath(filePath);
|
|
403
|
+
if (!normalized) return null;
|
|
404
|
+
|
|
405
|
+
const physicalPath = path.resolve(this.cachePath, normalized);
|
|
406
|
+
const cachePathWithSeparator = `${this.cachePath}${path.sep}`;
|
|
407
|
+
if (!physicalPath.startsWith(cachePathWithSeparator)) {
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
return physicalPath;
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
isValidUserId(value) {
|
|
415
|
+
return typeof value === 'string' && value.trim().length > 0 && value.length <= 256;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
normalizeLimit(limit) {
|
|
419
|
+
const value = Number(limit);
|
|
420
|
+
if (!Number.isFinite(value) || value <= 0) return DEFAULT_LIMIT;
|
|
421
|
+
return Math.min(Math.floor(value), MAX_LIMIT);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
normalizeCursor(cursor) {
|
|
425
|
+
if (cursor === null || cursor === undefined || cursor === '') return null;
|
|
426
|
+
const value = Number(cursor);
|
|
427
|
+
if (!Number.isInteger(value) || value < 0) {
|
|
428
|
+
throw new Error('Invalid history cursor');
|
|
429
|
+
}
|
|
430
|
+
return value;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
async updateCleanupFailure(filePath, errorMessage) {
|
|
434
|
+
await this.run(
|
|
435
|
+
`UPDATE file_cleanup_queue
|
|
436
|
+
SET retry_count = retry_count + 1, last_error = ?
|
|
437
|
+
WHERE file_path = ?`,
|
|
438
|
+
[errorMessage, filePath]
|
|
439
|
+
);
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
enqueueWrite(operation) {
|
|
443
|
+
const result = this.writeLock.then(operation);
|
|
444
|
+
this.writeLock = result.catch(() => {});
|
|
445
|
+
return result;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
ensureOpened() {
|
|
449
|
+
if (!this.db) {
|
|
450
|
+
throw new Error('Message history service is not initialized');
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
run(sql, params = []) {
|
|
455
|
+
return new Promise((resolve, reject) => {
|
|
456
|
+
this.db.run(sql, params, (error) => (error ? reject(error) : resolve()));
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
get(sql, params = []) {
|
|
461
|
+
return new Promise((resolve, reject) => {
|
|
462
|
+
this.db.get(sql, params, (error, row) => (error ? reject(error) : resolve(row)));
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
all(sql, params = []) {
|
|
467
|
+
return new Promise((resolve, reject) => {
|
|
468
|
+
this.db.all(sql, params, (error, rows) => (error ? reject(error) : resolve(rows || [])));
|
|
469
|
+
});
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
module.exports = {
|
|
474
|
+
MessageHistoryService,
|
|
475
|
+
GROUP_SESSION_ID
|
|
476
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
const moment = require('moment');
|
|
2
|
+
const crypto = require('crypto');
|
|
3
|
+
|
|
4
|
+
function decorateMessage(message, sender) {
|
|
5
|
+
const decorated = { ...message };
|
|
6
|
+
|
|
7
|
+
decorated.createTime = moment().format('YYYY-MM-DD HH:mm:ss');
|
|
8
|
+
decorated.msgId = crypto.randomUUID();
|
|
9
|
+
decorated.createdAt = Date.now();
|
|
10
|
+
decorated.senderPhotoNickname = decorated.sender.slice(0, 2);
|
|
11
|
+
|
|
12
|
+
if (sender) {
|
|
13
|
+
decorated.sender_nickname = sender.nickname || '';
|
|
14
|
+
decorated.sender_ip = sender.ip || '';
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return decorated;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function decoratePrivateMessage(message, sender) {
|
|
21
|
+
return decorateMessage(message, sender);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function decorateGroupMessage(message, sender) {
|
|
25
|
+
return decorateMessage(message, sender);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
decoratePrivateMessage,
|
|
30
|
+
decorateGroupMessage
|
|
31
|
+
};
|