@imenam/mcp-messenger 1.0.30 → 1.0.32
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/README.md +772 -727
- package/dist/src/cli/watch.d.ts +3 -1
- package/dist/src/cli/watch.js +12 -2
- package/dist/src/client/messenger-client.d.ts +5 -1
- package/dist/src/client/messenger-client.js +5 -0
- package/dist/src/core/store.d.ts +28 -1
- package/dist/src/core/store.js +315 -211
- package/dist/src/core/types.d.ts +25 -1
- package/dist/src/mcp/start.js +25 -22
- package/dist/src/mcp/tools.js +45 -7
- package/dist/src/server/app.js +53 -10
- package/dist/src/server/ui/app.js +2655 -1632
- package/dist/src/server/ui/index.html +284 -225
- package/dist/src/server/ui/styles.css +809 -21
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +16 -2
- package/package.json +2 -2
package/dist/src/core/store.js
CHANGED
|
@@ -22,14 +22,15 @@ const THREAD_DESCRIPTION_MAX = 2000;
|
|
|
22
22
|
const FILENAME_MAX = 255;
|
|
23
23
|
const MIME_TYPE_RE = /^[a-z0-9][a-z0-9!#$&^_.+-]*\/[a-z0-9][a-z0-9!#$&^_.+-]*$/;
|
|
24
24
|
const DEFAULT_MIME_TYPE = "application/octet-stream";
|
|
25
|
+
const LINK_NOTE_MAX = 500;
|
|
25
26
|
/**
|
|
26
27
|
* Dernière activité d'une boîte : sa dernière connexion, ou le dernier message qui l'a
|
|
27
28
|
* touchée. Une boîte qui ne fait que recevoir reste vivante, alors que `last_seen_at`
|
|
28
29
|
* seul la ferait paraître abandonnée.
|
|
29
30
|
*/
|
|
30
|
-
const LAST_ACTIVITY = `MAX(
|
|
31
|
-
COALESCE(a.last_seen_at, ''),
|
|
32
|
-
COALESCE((SELECT MAX(d.created_at) FROM deliveries d WHERE d.agent_id = a.id), '')
|
|
31
|
+
const LAST_ACTIVITY = `MAX(
|
|
32
|
+
COALESCE(a.last_seen_at, ''),
|
|
33
|
+
COALESCE((SELECT MAX(d.created_at) FROM deliveries d WHERE d.agent_id = a.id), '')
|
|
33
34
|
)`;
|
|
34
35
|
/** Colonnes du fil jointes à tout message : le titre voyage avec chaque livraison. */
|
|
35
36
|
const THREAD_JOIN = "LEFT JOIN threads t ON t.id = m.thread_id";
|
|
@@ -42,10 +43,10 @@ const THREAD_JOIN = "LEFT JOIN threads t ON t.id = m.thread_id";
|
|
|
42
43
|
* pose. Un message n'ayant qu'une copie d'expéditeur (destinataire jamais enregistré) n'a
|
|
43
44
|
* pas de ligne ici : `read_at` reste alors nul, donc non lu.
|
|
44
45
|
*/
|
|
45
|
-
const ADMIN_DELIVERY = `'to' AS role,
|
|
46
|
-
(SELECT dr.read_at FROM deliveries dr WHERE dr.message_id = m.id AND dr.role != 'sender'
|
|
47
|
-
ORDER BY dr.agent_id LIMIT 1) AS read_at,
|
|
48
|
-
COALESCE((SELECT dr.folder FROM deliveries dr WHERE dr.message_id = m.id AND dr.role != 'sender'
|
|
46
|
+
const ADMIN_DELIVERY = `'to' AS role,
|
|
47
|
+
(SELECT dr.read_at FROM deliveries dr WHERE dr.message_id = m.id AND dr.role != 'sender'
|
|
48
|
+
ORDER BY dr.agent_id LIMIT 1) AS read_at,
|
|
49
|
+
COALESCE((SELECT dr.folder FROM deliveries dr WHERE dr.message_id = m.id AND dr.role != 'sender'
|
|
49
50
|
ORDER BY dr.agent_id LIMIT 1), 'inbox') AS folder`;
|
|
50
51
|
/**
|
|
51
52
|
* Boîte aux lettres persistée en SQLite.
|
|
@@ -68,96 +69,108 @@ export class MessageStore extends EventEmitter {
|
|
|
68
69
|
this.migrate();
|
|
69
70
|
}
|
|
70
71
|
migrate() {
|
|
71
|
-
this.db.exec(`
|
|
72
|
-
CREATE TABLE IF NOT EXISTS agents (
|
|
73
|
-
id TEXT PRIMARY KEY,
|
|
74
|
-
agent_name TEXT,
|
|
75
|
-
agent_description TEXT,
|
|
76
|
-
display_name TEXT,
|
|
77
|
-
description TEXT,
|
|
78
|
-
created_at TEXT NOT NULL,
|
|
79
|
-
last_seen_at TEXT,
|
|
80
|
-
registered INTEGER NOT NULL DEFAULT 0
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
CREATE TABLE IF NOT EXISTS messages (
|
|
84
|
-
id TEXT PRIMARY KEY,
|
|
85
|
-
thread_id TEXT NOT NULL,
|
|
86
|
-
sender_id TEXT NOT NULL,
|
|
87
|
-
subject TEXT NOT NULL,
|
|
88
|
-
body TEXT NOT NULL,
|
|
89
|
-
priority TEXT NOT NULL DEFAULT 'normal',
|
|
90
|
-
in_reply_to TEXT,
|
|
91
|
-
created_at TEXT NOT NULL
|
|
92
|
-
);
|
|
93
|
-
|
|
94
|
-
CREATE TABLE IF NOT EXISTS deliveries (
|
|
95
|
-
id TEXT PRIMARY KEY,
|
|
96
|
-
message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
97
|
-
agent_id TEXT NOT NULL,
|
|
98
|
-
role TEXT NOT NULL,
|
|
99
|
-
folder TEXT NOT NULL,
|
|
100
|
-
read_at TEXT,
|
|
101
|
-
starred INTEGER NOT NULL DEFAULT 0,
|
|
102
|
-
created_at TEXT NOT NULL,
|
|
103
|
-
UNIQUE (message_id, agent_id, role)
|
|
104
|
-
);
|
|
105
|
-
|
|
106
|
-
CREATE TABLE IF NOT EXISTS threads (
|
|
107
|
-
id TEXT PRIMARY KEY,
|
|
108
|
-
title TEXT,
|
|
109
|
-
description TEXT,
|
|
110
|
-
named_by TEXT,
|
|
111
|
-
created_at TEXT NOT NULL,
|
|
112
|
-
updated_at TEXT
|
|
113
|
-
);
|
|
114
|
-
|
|
115
|
-
CREATE TABLE IF NOT EXISTS folders (
|
|
116
|
-
agent_id TEXT NOT NULL,
|
|
117
|
-
name TEXT NOT NULL,
|
|
118
|
-
created_at TEXT NOT NULL,
|
|
119
|
-
PRIMARY KEY (agent_id, name)
|
|
120
|
-
);
|
|
121
|
-
|
|
122
|
-
CREATE TABLE IF NOT EXISTS attachments (
|
|
123
|
-
id TEXT PRIMARY KEY,
|
|
124
|
-
message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
125
|
-
position INTEGER NOT NULL,
|
|
126
|
-
filename TEXT NOT NULL,
|
|
127
|
-
mime_type TEXT NOT NULL,
|
|
128
|
-
size INTEGER NOT NULL,
|
|
129
|
-
data BLOB NOT NULL,
|
|
130
|
-
created_at TEXT NOT NULL
|
|
131
|
-
);
|
|
132
|
-
|
|
133
|
-
CREATE
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
72
|
+
this.db.exec(`
|
|
73
|
+
CREATE TABLE IF NOT EXISTS agents (
|
|
74
|
+
id TEXT PRIMARY KEY,
|
|
75
|
+
agent_name TEXT,
|
|
76
|
+
agent_description TEXT,
|
|
77
|
+
display_name TEXT,
|
|
78
|
+
description TEXT,
|
|
79
|
+
created_at TEXT NOT NULL,
|
|
80
|
+
last_seen_at TEXT,
|
|
81
|
+
registered INTEGER NOT NULL DEFAULT 0
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
CREATE TABLE IF NOT EXISTS messages (
|
|
85
|
+
id TEXT PRIMARY KEY,
|
|
86
|
+
thread_id TEXT NOT NULL,
|
|
87
|
+
sender_id TEXT NOT NULL,
|
|
88
|
+
subject TEXT NOT NULL,
|
|
89
|
+
body TEXT NOT NULL,
|
|
90
|
+
priority TEXT NOT NULL DEFAULT 'normal',
|
|
91
|
+
in_reply_to TEXT,
|
|
92
|
+
created_at TEXT NOT NULL
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
CREATE TABLE IF NOT EXISTS deliveries (
|
|
96
|
+
id TEXT PRIMARY KEY,
|
|
97
|
+
message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
98
|
+
agent_id TEXT NOT NULL,
|
|
99
|
+
role TEXT NOT NULL,
|
|
100
|
+
folder TEXT NOT NULL,
|
|
101
|
+
read_at TEXT,
|
|
102
|
+
starred INTEGER NOT NULL DEFAULT 0,
|
|
103
|
+
created_at TEXT NOT NULL,
|
|
104
|
+
UNIQUE (message_id, agent_id, role)
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
CREATE TABLE IF NOT EXISTS threads (
|
|
108
|
+
id TEXT PRIMARY KEY,
|
|
109
|
+
title TEXT,
|
|
110
|
+
description TEXT,
|
|
111
|
+
named_by TEXT,
|
|
112
|
+
created_at TEXT NOT NULL,
|
|
113
|
+
updated_at TEXT
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
CREATE TABLE IF NOT EXISTS folders (
|
|
117
|
+
agent_id TEXT NOT NULL,
|
|
118
|
+
name TEXT NOT NULL,
|
|
119
|
+
created_at TEXT NOT NULL,
|
|
120
|
+
PRIMARY KEY (agent_id, name)
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
CREATE TABLE IF NOT EXISTS attachments (
|
|
124
|
+
id TEXT PRIMARY KEY,
|
|
125
|
+
message_id TEXT NOT NULL REFERENCES messages(id) ON DELETE CASCADE,
|
|
126
|
+
position INTEGER NOT NULL,
|
|
127
|
+
filename TEXT NOT NULL,
|
|
128
|
+
mime_type TEXT NOT NULL,
|
|
129
|
+
size INTEGER NOT NULL,
|
|
130
|
+
data BLOB NOT NULL,
|
|
131
|
+
created_at TEXT NOT NULL
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
CREATE TABLE IF NOT EXISTS links (
|
|
135
|
+
id TEXT PRIMARY KEY,
|
|
136
|
+
mailbox_a TEXT NOT NULL,
|
|
137
|
+
mailbox_b TEXT NOT NULL,
|
|
138
|
+
note TEXT,
|
|
139
|
+
created_at TEXT NOT NULL,
|
|
140
|
+
seen_by_a_at TEXT,
|
|
141
|
+
seen_by_b_at TEXT,
|
|
142
|
+
UNIQUE (mailbox_a, mailbox_b)
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
CREATE INDEX IF NOT EXISTS idx_links_b ON links (mailbox_b);
|
|
146
|
+
CREATE INDEX IF NOT EXISTS idx_attachments_message ON attachments (message_id, position);
|
|
147
|
+
CREATE INDEX IF NOT EXISTS idx_deliveries_mailbox ON deliveries (agent_id, folder, created_at DESC);
|
|
148
|
+
CREATE INDEX IF NOT EXISTS idx_deliveries_message ON deliveries (message_id);
|
|
149
|
+
CREATE INDEX IF NOT EXISTS idx_messages_thread ON messages (thread_id, created_at);
|
|
137
150
|
`);
|
|
138
151
|
// Bases antérieures à la séparation agent / boîte : `display_name` y tenait les deux
|
|
139
152
|
// rôles, et venait presque toujours de la configuration (le nom du projet). On le
|
|
140
153
|
// déplace vers l'identité de l'agent ; la boîte repart sans nom, à poser par l'agent.
|
|
141
154
|
const agentColumns = this.db.prepare("PRAGMA table_info(agents)").all().map((c) => c.name);
|
|
142
155
|
if (!agentColumns.includes("agent_name")) {
|
|
143
|
-
this.db.exec(`
|
|
144
|
-
ALTER TABLE agents ADD COLUMN agent_name TEXT;
|
|
145
|
-
ALTER TABLE agents ADD COLUMN agent_description TEXT;
|
|
146
|
-
UPDATE agents
|
|
147
|
-
SET agent_name = display_name,
|
|
148
|
-
agent_description = description,
|
|
149
|
-
display_name = NULL,
|
|
150
|
-
description = NULL;
|
|
156
|
+
this.db.exec(`
|
|
157
|
+
ALTER TABLE agents ADD COLUMN agent_name TEXT;
|
|
158
|
+
ALTER TABLE agents ADD COLUMN agent_description TEXT;
|
|
159
|
+
UPDATE agents
|
|
160
|
+
SET agent_name = display_name,
|
|
161
|
+
agent_description = description,
|
|
162
|
+
display_name = NULL,
|
|
163
|
+
description = NULL;
|
|
151
164
|
`);
|
|
152
165
|
}
|
|
153
166
|
// Bases antérieures aux fils nommés : chaque fil déjà présent reçoit sa ligne, sans
|
|
154
167
|
// titre. L'affichage retombe alors sur le sujet d'ouverture, comme avant.
|
|
155
|
-
this.db.exec(`
|
|
156
|
-
INSERT INTO threads (id, title, description, named_by, created_at, updated_at)
|
|
157
|
-
SELECT m.thread_id, NULL, NULL, NULL, MIN(m.created_at), NULL
|
|
158
|
-
FROM messages m
|
|
159
|
-
WHERE NOT EXISTS (SELECT 1 FROM threads t WHERE t.id = m.thread_id)
|
|
160
|
-
GROUP BY m.thread_id;
|
|
168
|
+
this.db.exec(`
|
|
169
|
+
INSERT INTO threads (id, title, description, named_by, created_at, updated_at)
|
|
170
|
+
SELECT m.thread_id, NULL, NULL, NULL, MIN(m.created_at), NULL
|
|
171
|
+
FROM messages m
|
|
172
|
+
WHERE NOT EXISTS (SELECT 1 FROM threads t WHERE t.id = m.thread_id)
|
|
173
|
+
GROUP BY m.thread_id;
|
|
161
174
|
`);
|
|
162
175
|
}
|
|
163
176
|
close() {
|
|
@@ -174,24 +187,34 @@ export class MessageStore extends EventEmitter {
|
|
|
174
187
|
const existing = this.db.prepare("SELECT id FROM agents WHERE id = ?").get(id);
|
|
175
188
|
if (!existing) {
|
|
176
189
|
this.db
|
|
177
|
-
.prepare(`INSERT INTO agents (id, agent_name, agent_description, display_name, description, created_at, last_seen_at, registered)
|
|
190
|
+
.prepare(`INSERT INTO agents (id, agent_name, agent_description, display_name, description, created_at, last_seen_at, registered)
|
|
178
191
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
179
192
|
.run(id, opts.agent_name ?? null, opts.agent_description ?? null, opts.display_name ?? null, opts.description ?? null, now, opts.registered ? now : null, opts.registered ? 1 : 0);
|
|
180
193
|
}
|
|
181
194
|
else {
|
|
182
195
|
this.db
|
|
183
|
-
.prepare(`UPDATE agents
|
|
184
|
-
SET agent_name = COALESCE(?, agent_name),
|
|
185
|
-
agent_description = COALESCE(?, agent_description),
|
|
186
|
-
display_name = COALESCE(?, display_name),
|
|
187
|
-
description = COALESCE(?, description),
|
|
188
|
-
registered = MAX(registered, ?),
|
|
189
|
-
last_seen_at = CASE WHEN ? = 1 THEN ? ELSE last_seen_at END
|
|
196
|
+
.prepare(`UPDATE agents
|
|
197
|
+
SET agent_name = COALESCE(?, agent_name),
|
|
198
|
+
agent_description = COALESCE(?, agent_description),
|
|
199
|
+
display_name = COALESCE(?, display_name),
|
|
200
|
+
description = COALESCE(?, description),
|
|
201
|
+
registered = MAX(registered, ?),
|
|
202
|
+
last_seen_at = CASE WHEN ? = 1 THEN ? ELSE last_seen_at END
|
|
190
203
|
WHERE id = ?`)
|
|
191
204
|
.run(opts.agent_name ?? null, opts.agent_description ?? null, opts.display_name ?? null, opts.description ?? null, opts.registered ? 1 : 0, opts.registered ? 1 : 0, now, id);
|
|
192
205
|
}
|
|
193
206
|
return this.getAgent(id);
|
|
194
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Nomme une boîte depuis la console. Le nom remplace celui en place, quel qu'en soit
|
|
210
|
+
* l'auteur ; null l'efface. La boîte doit exister, et sa date de passage ne bouge pas.
|
|
211
|
+
*/
|
|
212
|
+
setMailboxName(id, name) {
|
|
213
|
+
const result = this.db.prepare("UPDATE agents SET display_name = ? WHERE id = ?").run(name, id);
|
|
214
|
+
if (result.changes === 0)
|
|
215
|
+
throw new MessengerError(`mailbox not found: ${id}`, 404);
|
|
216
|
+
return this.getAgent(id);
|
|
217
|
+
}
|
|
195
218
|
touchAgent(id) {
|
|
196
219
|
this.db.prepare("UPDATE agents SET last_seen_at = ? WHERE id = ?").run(new Date().toISOString(), id);
|
|
197
220
|
}
|
|
@@ -208,9 +231,9 @@ export class MessageStore extends EventEmitter {
|
|
|
208
231
|
listAgents(exclude) {
|
|
209
232
|
const excluded = exclude ? normalizeAgentId(exclude) : "";
|
|
210
233
|
const rows = this.db
|
|
211
|
-
.prepare(`SELECT a.*, ${LAST_ACTIVITY} AS last_activity_at
|
|
212
|
-
FROM agents a
|
|
213
|
-
WHERE a.id != ?
|
|
234
|
+
.prepare(`SELECT a.*, ${LAST_ACTIVITY} AS last_activity_at
|
|
235
|
+
FROM agents a
|
|
236
|
+
WHERE a.id != ?
|
|
214
237
|
ORDER BY last_activity_at DESC, a.id`)
|
|
215
238
|
.all(excluded);
|
|
216
239
|
return rows.map((r) => ({
|
|
@@ -268,23 +291,23 @@ export class MessageStore extends EventEmitter {
|
|
|
268
291
|
// Le fil naît avec son premier message ; sur une réponse, la ligne existe déjà et
|
|
269
292
|
// n'est touchée que si l'expéditeur fournit un nouveau titre.
|
|
270
293
|
this.db
|
|
271
|
-
.prepare(`INSERT INTO threads (id, title, description, named_by, created_at, updated_at)
|
|
272
|
-
VALUES (?, NULL, NULL, NULL, ?, NULL)
|
|
294
|
+
.prepare(`INSERT INTO threads (id, title, description, named_by, created_at, updated_at)
|
|
295
|
+
VALUES (?, NULL, NULL, NULL, ?, NULL)
|
|
273
296
|
ON CONFLICT (id) DO NOTHING`)
|
|
274
297
|
.run(threadId, now);
|
|
275
298
|
if (title !== null || threadDescription !== null) {
|
|
276
299
|
this.renameThreadRow(threadId, from, title, threadDescription, now);
|
|
277
300
|
}
|
|
278
301
|
this.db
|
|
279
|
-
.prepare(`INSERT INTO messages (id, thread_id, sender_id, subject, body, priority, in_reply_to, created_at)
|
|
302
|
+
.prepare(`INSERT INTO messages (id, thread_id, sender_id, subject, body, priority, in_reply_to, created_at)
|
|
280
303
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
|
|
281
304
|
.run(messageId, threadId, from, subject, input.body, priority, inReplyTo, now);
|
|
282
|
-
const insertAttachment = this.db.prepare(`INSERT INTO attachments (id, message_id, position, filename, mime_type, size, data, created_at)
|
|
305
|
+
const insertAttachment = this.db.prepare(`INSERT INTO attachments (id, message_id, position, filename, mime_type, size, data, created_at)
|
|
283
306
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`);
|
|
284
307
|
attachments.forEach((a, position) => {
|
|
285
308
|
insertAttachment.run(newId("att"), messageId, position, a.filename, a.mime_type, a.data.length, a.data, now);
|
|
286
309
|
});
|
|
287
|
-
const insert = this.db.prepare(`INSERT INTO deliveries (id, message_id, agent_id, role, folder, read_at, starred, created_at)
|
|
310
|
+
const insert = this.db.prepare(`INSERT INTO deliveries (id, message_id, agent_id, role, folder, read_at, starred, created_at)
|
|
288
311
|
VALUES (?, ?, ?, ?, ?, ?, 0, ?)`);
|
|
289
312
|
for (const d of deliveries) {
|
|
290
313
|
insert.run(newId("dlv"), messageId, d.agent_id, d.role, d.folder, d.read_at, now);
|
|
@@ -329,13 +352,13 @@ export class MessageStore extends EventEmitter {
|
|
|
329
352
|
params.push(like, like);
|
|
330
353
|
}
|
|
331
354
|
const rows = this.db
|
|
332
|
-
.prepare(`SELECT m.id AS message_id, m.thread_id, t.title AS thread_title, m.sender_id, m.subject, m.body,
|
|
333
|
-
m.priority, m.in_reply_to, m.created_at, d.folder, d.role, d.read_at, d.starred
|
|
334
|
-
FROM deliveries d
|
|
335
|
-
JOIN messages m ON m.id = d.message_id
|
|
336
|
-
${THREAD_JOIN}
|
|
337
|
-
WHERE ${where.join(" AND ")}
|
|
338
|
-
ORDER BY m.created_at DESC, m.id DESC
|
|
355
|
+
.prepare(`SELECT m.id AS message_id, m.thread_id, t.title AS thread_title, m.sender_id, m.subject, m.body,
|
|
356
|
+
m.priority, m.in_reply_to, m.created_at, d.folder, d.role, d.read_at, d.starred
|
|
357
|
+
FROM deliveries d
|
|
358
|
+
JOIN messages m ON m.id = d.message_id
|
|
359
|
+
${THREAD_JOIN}
|
|
360
|
+
WHERE ${where.join(" AND ")}
|
|
361
|
+
ORDER BY m.created_at DESC, m.id DESC
|
|
339
362
|
LIMIT ? OFFSET ?`)
|
|
340
363
|
.all(...params, limit, offset);
|
|
341
364
|
const related = this.relatedFor(rows.map((r) => r.message_id));
|
|
@@ -343,13 +366,13 @@ export class MessageStore extends EventEmitter {
|
|
|
343
366
|
}
|
|
344
367
|
getMessage(agentId, messageId, markRead = false) {
|
|
345
368
|
const row = this.db
|
|
346
|
-
.prepare(`SELECT m.id AS message_id, m.thread_id, t.title AS thread_title, m.sender_id, m.subject, m.body,
|
|
347
|
-
m.priority, m.in_reply_to, m.created_at, d.folder, d.role, d.read_at, d.starred
|
|
348
|
-
FROM deliveries d
|
|
349
|
-
JOIN messages m ON m.id = d.message_id
|
|
350
|
-
${THREAD_JOIN}
|
|
351
|
-
WHERE d.agent_id = ? AND d.message_id = ?
|
|
352
|
-
ORDER BY CASE d.role WHEN 'to' THEN 0 ELSE 1 END
|
|
369
|
+
.prepare(`SELECT m.id AS message_id, m.thread_id, t.title AS thread_title, m.sender_id, m.subject, m.body,
|
|
370
|
+
m.priority, m.in_reply_to, m.created_at, d.folder, d.role, d.read_at, d.starred
|
|
371
|
+
FROM deliveries d
|
|
372
|
+
JOIN messages m ON m.id = d.message_id
|
|
373
|
+
${THREAD_JOIN}
|
|
374
|
+
WHERE d.agent_id = ? AND d.message_id = ?
|
|
375
|
+
ORDER BY CASE d.role WHEN 'to' THEN 0 ELSE 1 END
|
|
353
376
|
LIMIT 1`)
|
|
354
377
|
.get(agentId, messageId);
|
|
355
378
|
if (!row)
|
|
@@ -367,14 +390,14 @@ export class MessageStore extends EventEmitter {
|
|
|
367
390
|
/** Tous les messages d'un fil visibles par l'agent, du plus ancien au plus récent. */
|
|
368
391
|
getThread(agentId, threadId) {
|
|
369
392
|
const rows = this.db
|
|
370
|
-
.prepare(`SELECT m.id AS message_id, m.thread_id, t.title AS thread_title, m.sender_id, m.subject, m.body,
|
|
371
|
-
m.priority, m.in_reply_to, m.created_at,
|
|
372
|
-
MIN(d.folder) AS folder, MIN(d.role) AS role, MIN(d.read_at) AS read_at, MAX(d.starred) AS starred
|
|
373
|
-
FROM deliveries d
|
|
374
|
-
JOIN messages m ON m.id = d.message_id
|
|
375
|
-
${THREAD_JOIN}
|
|
376
|
-
WHERE d.agent_id = ? AND m.thread_id = ?
|
|
377
|
-
GROUP BY m.id
|
|
393
|
+
.prepare(`SELECT m.id AS message_id, m.thread_id, t.title AS thread_title, m.sender_id, m.subject, m.body,
|
|
394
|
+
m.priority, m.in_reply_to, m.created_at,
|
|
395
|
+
MIN(d.folder) AS folder, MIN(d.role) AS role, MIN(d.read_at) AS read_at, MAX(d.starred) AS starred
|
|
396
|
+
FROM deliveries d
|
|
397
|
+
JOIN messages m ON m.id = d.message_id
|
|
398
|
+
${THREAD_JOIN}
|
|
399
|
+
WHERE d.agent_id = ? AND m.thread_id = ?
|
|
400
|
+
GROUP BY m.id
|
|
378
401
|
ORDER BY m.created_at ASC`)
|
|
379
402
|
.all(agentId, threadId);
|
|
380
403
|
if (rows.length === 0)
|
|
@@ -385,9 +408,9 @@ export class MessageStore extends EventEmitter {
|
|
|
385
408
|
/** Pièce jointe d'un message livré à cet agent, contenu compris. */
|
|
386
409
|
getAttachment(agentId, attachmentId) {
|
|
387
410
|
const row = this.db
|
|
388
|
-
.prepare(`SELECT a.id AS attachment_id, a.message_id, a.filename, a.mime_type, a.size, a.data
|
|
389
|
-
FROM attachments a
|
|
390
|
-
WHERE a.id = ?
|
|
411
|
+
.prepare(`SELECT a.id AS attachment_id, a.message_id, a.filename, a.mime_type, a.size, a.data
|
|
412
|
+
FROM attachments a
|
|
413
|
+
WHERE a.id = ?
|
|
391
414
|
AND EXISTS (SELECT 1 FROM deliveries d WHERE d.message_id = a.message_id AND d.agent_id = ?)`)
|
|
392
415
|
.get(attachmentId, agentId);
|
|
393
416
|
if (!row)
|
|
@@ -432,7 +455,7 @@ export class MessageStore extends EventEmitter {
|
|
|
432
455
|
// qu'elle ait écrit ou seulement reçu. Les paramètres du WHERE se lient avant ceux du
|
|
433
456
|
// HAVING : cette condition doit donc être poussée avant celles de la recherche.
|
|
434
457
|
if (opts.with_agent) {
|
|
435
|
-
where.push(`EXISTS (SELECT 1 FROM deliveries d2 JOIN messages m2 ON m2.id = d2.message_id
|
|
458
|
+
where.push(`EXISTS (SELECT 1 FROM deliveries d2 JOIN messages m2 ON m2.id = d2.message_id
|
|
436
459
|
WHERE m2.thread_id = m.thread_id AND d2.agent_id = ?)`);
|
|
437
460
|
params.push(normalizeAgentId(opts.with_agent));
|
|
438
461
|
}
|
|
@@ -445,20 +468,20 @@ export class MessageStore extends EventEmitter {
|
|
|
445
468
|
params.push(like, like, like);
|
|
446
469
|
}
|
|
447
470
|
const rows = this.db
|
|
448
|
-
.prepare(`SELECT m.thread_id,
|
|
449
|
-
t.title, t.description, t.named_by, t.created_at, t.updated_at,
|
|
450
|
-
(SELECT r.subject FROM messages r WHERE r.thread_id = m.thread_id
|
|
451
|
-
ORDER BY r.created_at ASC, r.id ASC LIMIT 1) AS subject,
|
|
452
|
-
COUNT(DISTINCT m.id) AS messages,
|
|
453
|
-
COUNT(DISTINCT CASE WHEN d.read_at IS NULL AND d.role != 'sender' THEN m.id END) AS unread,
|
|
454
|
-
MAX(m.created_at) AS last_message_at
|
|
455
|
-
FROM deliveries d
|
|
456
|
-
JOIN messages m ON m.id = d.message_id
|
|
457
|
-
${THREAD_JOIN}
|
|
458
|
-
WHERE ${where.join(" AND ")}
|
|
459
|
-
GROUP BY m.thread_id
|
|
460
|
-
${having.length ? `HAVING ${having.join(" AND ")}` : ""}
|
|
461
|
-
ORDER BY last_message_at DESC
|
|
471
|
+
.prepare(`SELECT m.thread_id,
|
|
472
|
+
t.title, t.description, t.named_by, t.created_at, t.updated_at,
|
|
473
|
+
(SELECT r.subject FROM messages r WHERE r.thread_id = m.thread_id
|
|
474
|
+
ORDER BY r.created_at ASC, r.id ASC LIMIT 1) AS subject,
|
|
475
|
+
COUNT(DISTINCT m.id) AS messages,
|
|
476
|
+
COUNT(DISTINCT CASE WHEN d.read_at IS NULL AND d.role != 'sender' THEN m.id END) AS unread,
|
|
477
|
+
MAX(m.created_at) AS last_message_at
|
|
478
|
+
FROM deliveries d
|
|
479
|
+
JOIN messages m ON m.id = d.message_id
|
|
480
|
+
${THREAD_JOIN}
|
|
481
|
+
WHERE ${where.join(" AND ")}
|
|
482
|
+
GROUP BY m.thread_id
|
|
483
|
+
${having.length ? `HAVING ${having.join(" AND ")}` : ""}
|
|
484
|
+
ORDER BY last_message_at DESC
|
|
462
485
|
LIMIT ? OFFSET ?`)
|
|
463
486
|
.all(...params, limit, offset);
|
|
464
487
|
const participants = this.participantsFor(rows.map((r) => r.thread_id));
|
|
@@ -481,18 +504,18 @@ export class MessageStore extends EventEmitter {
|
|
|
481
504
|
/** Écriture brute du nom : un champ absent (`null`) laisse l'ancienne valeur en place. */
|
|
482
505
|
renameThreadRow(threadId, agentId, title, description, now) {
|
|
483
506
|
this.db
|
|
484
|
-
.prepare(`INSERT INTO threads (id, title, description, named_by, created_at, updated_at)
|
|
485
|
-
VALUES (?, ?, ?, ?, ?, ?)
|
|
486
|
-
ON CONFLICT (id) DO UPDATE SET
|
|
487
|
-
title = COALESCE(excluded.title, threads.title),
|
|
488
|
-
description = COALESCE(excluded.description, threads.description),
|
|
489
|
-
named_by = excluded.named_by,
|
|
507
|
+
.prepare(`INSERT INTO threads (id, title, description, named_by, created_at, updated_at)
|
|
508
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
509
|
+
ON CONFLICT (id) DO UPDATE SET
|
|
510
|
+
title = COALESCE(excluded.title, threads.title),
|
|
511
|
+
description = COALESCE(excluded.description, threads.description),
|
|
512
|
+
named_by = excluded.named_by,
|
|
490
513
|
updated_at = excluded.updated_at`)
|
|
491
514
|
.run(threadId, title, description, agentId, now, now);
|
|
492
515
|
}
|
|
493
516
|
assertThreadVisible(agentId, threadId) {
|
|
494
517
|
const row = this.db
|
|
495
|
-
.prepare(`SELECT 1 FROM deliveries d JOIN messages m ON m.id = d.message_id
|
|
518
|
+
.prepare(`SELECT 1 FROM deliveries d JOIN messages m ON m.id = d.message_id
|
|
496
519
|
WHERE d.agent_id = ? AND m.thread_id = ? LIMIT 1`)
|
|
497
520
|
.get(agentId, threadId);
|
|
498
521
|
if (!row)
|
|
@@ -505,9 +528,9 @@ export class MessageStore extends EventEmitter {
|
|
|
505
528
|
return map;
|
|
506
529
|
const placeholders = ids.map(() => "?").join(",");
|
|
507
530
|
const rows = this.db
|
|
508
|
-
.prepare(`SELECT DISTINCT m.thread_id, d.agent_id
|
|
509
|
-
FROM deliveries d JOIN messages m ON m.id = d.message_id
|
|
510
|
-
WHERE m.thread_id IN (${placeholders})
|
|
531
|
+
.prepare(`SELECT DISTINCT m.thread_id, d.agent_id
|
|
532
|
+
FROM deliveries d JOIN messages m ON m.id = d.message_id
|
|
533
|
+
WHERE m.thread_id IN (${placeholders})
|
|
511
534
|
ORDER BY d.agent_id`)
|
|
512
535
|
.all(...ids);
|
|
513
536
|
for (const row of rows) {
|
|
@@ -553,7 +576,7 @@ export class MessageStore extends EventEmitter {
|
|
|
553
576
|
listFolders(agentId) {
|
|
554
577
|
const custom = this.db.prepare("SELECT name FROM folders WHERE agent_id = ? ORDER BY name").all(agentId);
|
|
555
578
|
const counts = this.db
|
|
556
|
-
.prepare(`SELECT folder, COUNT(*) AS total, SUM(CASE WHEN read_at IS NULL THEN 1 ELSE 0 END) AS unread
|
|
579
|
+
.prepare(`SELECT folder, COUNT(*) AS total, SUM(CASE WHEN read_at IS NULL THEN 1 ELSE 0 END) AS unread
|
|
557
580
|
FROM deliveries WHERE agent_id = ? GROUP BY folder`)
|
|
558
581
|
.all(agentId);
|
|
559
582
|
const byName = new Map(counts.map((c) => [c.folder, c]));
|
|
@@ -592,7 +615,7 @@ export class MessageStore extends EventEmitter {
|
|
|
592
615
|
if (!exists)
|
|
593
616
|
throw new MessengerError(`folder not found: ${folder}`, 404);
|
|
594
617
|
const moved = this.db
|
|
595
|
-
.prepare(`UPDATE deliveries SET folder = CASE role WHEN 'sender' THEN 'sent' ELSE 'inbox' END
|
|
618
|
+
.prepare(`UPDATE deliveries SET folder = CASE role WHEN 'sender' THEN 'sent' ELSE 'inbox' END
|
|
596
619
|
WHERE agent_id = ? AND folder = ?`)
|
|
597
620
|
.run(agentId, folder).changes;
|
|
598
621
|
this.db.prepare("DELETE FROM folders WHERE agent_id = ? AND name = ?").run(agentId, folder);
|
|
@@ -625,10 +648,10 @@ export class MessageStore extends EventEmitter {
|
|
|
625
648
|
*/
|
|
626
649
|
staleMailboxes(before) {
|
|
627
650
|
const rows = this.db
|
|
628
|
-
.prepare(`SELECT a.*, ${LAST_ACTIVITY} AS last_activity_at,
|
|
629
|
-
(SELECT COUNT(*) FROM deliveries d WHERE d.agent_id = a.id) AS messages
|
|
630
|
-
FROM agents a
|
|
631
|
-
WHERE MAX(${LAST_ACTIVITY}, a.created_at) < ?
|
|
651
|
+
.prepare(`SELECT a.*, ${LAST_ACTIVITY} AS last_activity_at,
|
|
652
|
+
(SELECT COUNT(*) FROM deliveries d WHERE d.agent_id = a.id) AS messages
|
|
653
|
+
FROM agents a
|
|
654
|
+
WHERE MAX(${LAST_ACTIVITY}, a.created_at) < ?
|
|
632
655
|
ORDER BY MAX(${LAST_ACTIVITY}, a.created_at) ASC`)
|
|
633
656
|
.all(before);
|
|
634
657
|
return rows.map((row) => ({
|
|
@@ -647,10 +670,10 @@ export class MessageStore extends EventEmitter {
|
|
|
647
670
|
*/
|
|
648
671
|
mailboxesCreatedBefore(before) {
|
|
649
672
|
const rows = this.db
|
|
650
|
-
.prepare(`SELECT a.*, ${LAST_ACTIVITY} AS last_activity_at,
|
|
651
|
-
(SELECT COUNT(*) FROM deliveries d WHERE d.agent_id = a.id) AS messages
|
|
652
|
-
FROM agents a
|
|
653
|
-
WHERE a.created_at < ?
|
|
673
|
+
.prepare(`SELECT a.*, ${LAST_ACTIVITY} AS last_activity_at,
|
|
674
|
+
(SELECT COUNT(*) FROM deliveries d WHERE d.agent_id = a.id) AS messages
|
|
675
|
+
FROM agents a
|
|
676
|
+
WHERE a.created_at < ?
|
|
654
677
|
ORDER BY a.created_at ASC`)
|
|
655
678
|
.all(before);
|
|
656
679
|
return rows.map((row) => ({
|
|
@@ -669,9 +692,9 @@ export class MessageStore extends EventEmitter {
|
|
|
669
692
|
*/
|
|
670
693
|
emptyMailboxes() {
|
|
671
694
|
const rows = this.db
|
|
672
|
-
.prepare(`SELECT a.*, ${LAST_ACTIVITY} AS last_activity_at, 0 AS messages
|
|
673
|
-
FROM agents a
|
|
674
|
-
WHERE NOT EXISTS (SELECT 1 FROM deliveries d WHERE d.agent_id = a.id)
|
|
695
|
+
.prepare(`SELECT a.*, ${LAST_ACTIVITY} AS last_activity_at, 0 AS messages
|
|
696
|
+
FROM agents a
|
|
697
|
+
WHERE NOT EXISTS (SELECT 1 FROM deliveries d WHERE d.agent_id = a.id)
|
|
675
698
|
ORDER BY a.created_at ASC`)
|
|
676
699
|
.all();
|
|
677
700
|
return rows.map((row) => ({
|
|
@@ -699,9 +722,11 @@ export class MessageStore extends EventEmitter {
|
|
|
699
722
|
const dropDeliveries = this.db.prepare("DELETE FROM deliveries WHERE agent_id = ?");
|
|
700
723
|
const dropFolders = this.db.prepare("DELETE FROM folders WHERE agent_id = ?");
|
|
701
724
|
const dropAgent = this.db.prepare("DELETE FROM agents WHERE id = ?");
|
|
725
|
+
const dropLinks = this.db.prepare("DELETE FROM links WHERE mailbox_a = ? OR mailbox_b = ?");
|
|
702
726
|
for (const id of ids) {
|
|
703
727
|
deliveries += dropDeliveries.run(id).changes;
|
|
704
728
|
dropFolders.run(id);
|
|
729
|
+
dropLinks.run(id, id);
|
|
705
730
|
mailboxes += dropAgent.run(id).changes;
|
|
706
731
|
}
|
|
707
732
|
this.dropOrphanMessages();
|
|
@@ -715,6 +740,74 @@ export class MessageStore extends EventEmitter {
|
|
|
715
740
|
};
|
|
716
741
|
})();
|
|
717
742
|
}
|
|
743
|
+
// --- Liaisons -----------------------------------------------------------
|
|
744
|
+
/**
|
|
745
|
+
* Lie deux boîtes existantes. La paire est stockée dans l'ordre des adresses : une
|
|
746
|
+
* liaison n'a pas de direction, et la même paire ne peut être liée deux fois.
|
|
747
|
+
*/
|
|
748
|
+
createLink(first, second, note) {
|
|
749
|
+
const ids = [normalizeAgentId(first), normalizeAgentId(second)];
|
|
750
|
+
if (!ids[0] || !ids[1])
|
|
751
|
+
throw new MessengerError("two mailbox addresses are required");
|
|
752
|
+
if (ids[0] === ids[1])
|
|
753
|
+
throw new MessengerError("a mailbox cannot be linked to itself");
|
|
754
|
+
for (const id of ids) {
|
|
755
|
+
if (!this.getAgent(id))
|
|
756
|
+
throw new MessengerError(`mailbox not found: ${id}`, 404);
|
|
757
|
+
}
|
|
758
|
+
const [a, b] = ids.sort();
|
|
759
|
+
if (this.db.prepare("SELECT 1 FROM links WHERE mailbox_a = ? AND mailbox_b = ?").get(a, b)) {
|
|
760
|
+
throw new MessengerError(`these mailboxes are already linked: ${a}, ${b}`, 409);
|
|
761
|
+
}
|
|
762
|
+
const id = newId("lnk");
|
|
763
|
+
this.db
|
|
764
|
+
.prepare("INSERT INTO links (id, mailbox_a, mailbox_b, note, created_at) VALUES (?, ?, ?, ?, ?)")
|
|
765
|
+
.run(id, a, b, normalizeLabel(note, LINK_NOTE_MAX, "note"), new Date().toISOString());
|
|
766
|
+
for (const agentId of [a, b])
|
|
767
|
+
this.emit("link", { agent_id: agentId, link_id: id });
|
|
768
|
+
return this.getLink(id);
|
|
769
|
+
}
|
|
770
|
+
getLink(linkId) {
|
|
771
|
+
const row = this.db.prepare("SELECT * FROM links WHERE id = ?").get(linkId);
|
|
772
|
+
return row ? toLink(row) : null;
|
|
773
|
+
}
|
|
774
|
+
/** Toutes les liaisons du serveur, la plus récente en tête. */
|
|
775
|
+
listLinks() {
|
|
776
|
+
const rows = this.db.prepare("SELECT * FROM links ORDER BY created_at DESC, rowid DESC").all();
|
|
777
|
+
return rows.map(toLink);
|
|
778
|
+
}
|
|
779
|
+
deleteLink(linkId) {
|
|
780
|
+
const removed = this.db.prepare("DELETE FROM links WHERE id = ?").run(linkId).changes;
|
|
781
|
+
if (removed === 0)
|
|
782
|
+
throw new MessengerError(`link not found: ${linkId}`, 404);
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* Liaisons d'une boîte, la plus récente en tête, chacune avec la fiche de la boîte d'en
|
|
786
|
+
* face. `markSeen` note que l'agent de cette boîte en a pris connaissance.
|
|
787
|
+
*/
|
|
788
|
+
linksFor(agentId, opts = {}) {
|
|
789
|
+
const id = normalizeAgentId(agentId);
|
|
790
|
+
const where = ["(mailbox_a = ? OR mailbox_b = ?)"];
|
|
791
|
+
const params = [id, id];
|
|
792
|
+
if (opts.linkId) {
|
|
793
|
+
where.push("id = ?");
|
|
794
|
+
params.push(opts.linkId);
|
|
795
|
+
}
|
|
796
|
+
const rows = this.db
|
|
797
|
+
.prepare(`SELECT * FROM links WHERE ${where.join(" AND ")} ORDER BY created_at DESC, rowid DESC`)
|
|
798
|
+
.all(...params);
|
|
799
|
+
if (opts.markSeen && rows.length > 0) {
|
|
800
|
+
const now = new Date().toISOString();
|
|
801
|
+
this.db.prepare("UPDATE links SET seen_by_a_at = ? WHERE mailbox_a = ? AND seen_by_a_at IS NULL").run(now, id);
|
|
802
|
+
this.db.prepare("UPDATE links SET seen_by_b_at = ? WHERE mailbox_b = ? AND seen_by_b_at IS NULL").run(now, id);
|
|
803
|
+
}
|
|
804
|
+
return rows.map((row) => ({
|
|
805
|
+
link_id: row.id,
|
|
806
|
+
note: row.note,
|
|
807
|
+
created_at: row.created_at,
|
|
808
|
+
correspondent: this.getAgent(row.mailbox_a === id ? row.mailbox_b : row.mailbox_a),
|
|
809
|
+
}));
|
|
810
|
+
}
|
|
718
811
|
// --- Supervision (interface web, lecture seule sauf purge) ---------------
|
|
719
812
|
/** Vue globale : tous les messages du serveur, indépendamment des boîtes. */
|
|
720
813
|
listAllMessages(opts = {}) {
|
|
@@ -736,12 +829,12 @@ export class MessageStore extends EventEmitter {
|
|
|
736
829
|
params.push(opts.thread_id);
|
|
737
830
|
}
|
|
738
831
|
const rows = this.db
|
|
739
|
-
.prepare(`SELECT m.id AS message_id, m.thread_id, t.title AS thread_title, m.sender_id, m.subject, m.body,
|
|
740
|
-
m.priority, m.in_reply_to, m.created_at, 0 AS starred, ${ADMIN_DELIVERY}
|
|
741
|
-
FROM messages m
|
|
742
|
-
${THREAD_JOIN}
|
|
743
|
-
WHERE ${where.join(" AND ")}
|
|
744
|
-
ORDER BY m.created_at DESC, m.id DESC
|
|
832
|
+
.prepare(`SELECT m.id AS message_id, m.thread_id, t.title AS thread_title, m.sender_id, m.subject, m.body,
|
|
833
|
+
m.priority, m.in_reply_to, m.created_at, 0 AS starred, ${ADMIN_DELIVERY}
|
|
834
|
+
FROM messages m
|
|
835
|
+
${THREAD_JOIN}
|
|
836
|
+
WHERE ${where.join(" AND ")}
|
|
837
|
+
ORDER BY m.created_at DESC, m.id DESC
|
|
745
838
|
LIMIT ? OFFSET ?`)
|
|
746
839
|
.all(...params, limit, offset);
|
|
747
840
|
const related = this.relatedFor(rows.map((r) => r.message_id));
|
|
@@ -764,7 +857,7 @@ export class MessageStore extends EventEmitter {
|
|
|
764
857
|
// reçu : c'est la correspondance qui est filtrée, pas les messages qu'elle a signés.
|
|
765
858
|
const agents = dedupe((opts.agents ?? []).map((id) => normalizeAgentId(id)).filter(Boolean));
|
|
766
859
|
if (agents.length > 0) {
|
|
767
|
-
where.push(`EXISTS (SELECT 1 FROM deliveries d JOIN messages mp ON mp.id = d.message_id
|
|
860
|
+
where.push(`EXISTS (SELECT 1 FROM deliveries d JOIN messages mp ON mp.id = d.message_id
|
|
768
861
|
WHERE mp.thread_id = m.thread_id AND d.agent_id IN (${agents.map(() => "?").join(",")}))`);
|
|
769
862
|
params.push(...agents);
|
|
770
863
|
}
|
|
@@ -772,8 +865,8 @@ export class MessageStore extends EventEmitter {
|
|
|
772
865
|
// n'importe lequel de ses messages. Un fil non nommé reste ainsi trouvable.
|
|
773
866
|
if (opts.search) {
|
|
774
867
|
const like = `%${escapeLike(opts.search)}%`;
|
|
775
|
-
where.push(`(COALESCE(t.title, '') LIKE ? ESCAPE '\\' OR COALESCE(t.description, '') LIKE ? ESCAPE '\\'
|
|
776
|
-
OR EXISTS (SELECT 1 FROM messages ms WHERE ms.thread_id = m.thread_id
|
|
868
|
+
where.push(`(COALESCE(t.title, '') LIKE ? ESCAPE '\\' OR COALESCE(t.description, '') LIKE ? ESCAPE '\\'
|
|
869
|
+
OR EXISTS (SELECT 1 FROM messages ms WHERE ms.thread_id = m.thread_id
|
|
777
870
|
AND (ms.subject LIKE ? ESCAPE '\\' OR ms.body LIKE ? ESCAPE '\\')))`);
|
|
778
871
|
params.push(like, like, like, like);
|
|
779
872
|
}
|
|
@@ -782,28 +875,28 @@ export class MessageStore extends EventEmitter {
|
|
|
782
875
|
if (opts.priority)
|
|
783
876
|
having.push("top_priority = ?");
|
|
784
877
|
const rows = this.db
|
|
785
|
-
.prepare(`SELECT m.thread_id,
|
|
786
|
-
t.title, t.description, t.named_by, t.created_at, t.updated_at,
|
|
787
|
-
(SELECT r.subject FROM messages r WHERE r.thread_id = m.thread_id
|
|
788
|
-
ORDER BY r.created_at ASC, r.id ASC LIMIT 1) AS subject,
|
|
789
|
-
COUNT(*) AS messages,
|
|
790
|
-
MAX(m.created_at) AS last_message_at,
|
|
791
|
-
(SELECT r.sender_id FROM messages r WHERE r.thread_id = m.thread_id
|
|
792
|
-
ORDER BY r.created_at DESC, r.id DESC LIMIT 1) AS last_message_from,
|
|
793
|
-
(SELECT r.body FROM messages r WHERE r.thread_id = m.thread_id
|
|
794
|
-
ORDER BY r.created_at DESC, r.id DESC LIMIT 1) AS last_body,
|
|
795
|
-
(SELECT COUNT(*) FROM deliveries d JOIN messages mu ON mu.id = d.message_id
|
|
796
|
-
WHERE mu.thread_id = m.thread_id AND d.role != 'sender'
|
|
797
|
-
AND d.read_at IS NULL AND d.folder != 'trash') AS unread,
|
|
798
|
-
CASE WHEN SUM(m.priority = 'high') > 0 THEN 'high'
|
|
799
|
-
WHEN SUM(m.priority = 'normal') > 0 THEN 'normal'
|
|
800
|
-
ELSE 'low' END AS top_priority
|
|
801
|
-
FROM messages m
|
|
802
|
-
${THREAD_JOIN}
|
|
803
|
-
WHERE ${where.join(" AND ")}
|
|
804
|
-
GROUP BY m.thread_id
|
|
805
|
-
${having.length ? `HAVING ${having.join(" AND ")}` : ""}
|
|
806
|
-
ORDER BY last_message_at DESC
|
|
878
|
+
.prepare(`SELECT m.thread_id,
|
|
879
|
+
t.title, t.description, t.named_by, t.created_at, t.updated_at,
|
|
880
|
+
(SELECT r.subject FROM messages r WHERE r.thread_id = m.thread_id
|
|
881
|
+
ORDER BY r.created_at ASC, r.id ASC LIMIT 1) AS subject,
|
|
882
|
+
COUNT(*) AS messages,
|
|
883
|
+
MAX(m.created_at) AS last_message_at,
|
|
884
|
+
(SELECT r.sender_id FROM messages r WHERE r.thread_id = m.thread_id
|
|
885
|
+
ORDER BY r.created_at DESC, r.id DESC LIMIT 1) AS last_message_from,
|
|
886
|
+
(SELECT r.body FROM messages r WHERE r.thread_id = m.thread_id
|
|
887
|
+
ORDER BY r.created_at DESC, r.id DESC LIMIT 1) AS last_body,
|
|
888
|
+
(SELECT COUNT(*) FROM deliveries d JOIN messages mu ON mu.id = d.message_id
|
|
889
|
+
WHERE mu.thread_id = m.thread_id AND d.role != 'sender'
|
|
890
|
+
AND d.read_at IS NULL AND d.folder != 'trash') AS unread,
|
|
891
|
+
CASE WHEN SUM(m.priority = 'high') > 0 THEN 'high'
|
|
892
|
+
WHEN SUM(m.priority = 'normal') > 0 THEN 'normal'
|
|
893
|
+
ELSE 'low' END AS top_priority
|
|
894
|
+
FROM messages m
|
|
895
|
+
${THREAD_JOIN}
|
|
896
|
+
WHERE ${where.join(" AND ")}
|
|
897
|
+
GROUP BY m.thread_id
|
|
898
|
+
${having.length ? `HAVING ${having.join(" AND ")}` : ""}
|
|
899
|
+
ORDER BY last_message_at DESC
|
|
807
900
|
LIMIT ? OFFSET ?`)
|
|
808
901
|
.all(...params, ...(opts.priority ? [opts.priority] : []), limit, offset);
|
|
809
902
|
const participants = this.participantsFor(rows.map((r) => r.thread_id));
|
|
@@ -833,11 +926,11 @@ export class MessageStore extends EventEmitter {
|
|
|
833
926
|
*/
|
|
834
927
|
listThreadAdmin(threadId) {
|
|
835
928
|
const rows = this.db
|
|
836
|
-
.prepare(`SELECT m.id AS message_id, m.thread_id, t.title AS thread_title, m.sender_id, m.subject, m.body,
|
|
837
|
-
m.priority, m.in_reply_to, m.created_at, 0 AS starred, ${ADMIN_DELIVERY}
|
|
838
|
-
FROM messages m
|
|
839
|
-
${THREAD_JOIN}
|
|
840
|
-
WHERE m.thread_id = ?
|
|
929
|
+
.prepare(`SELECT m.id AS message_id, m.thread_id, t.title AS thread_title, m.sender_id, m.subject, m.body,
|
|
930
|
+
m.priority, m.in_reply_to, m.created_at, 0 AS starred, ${ADMIN_DELIVERY}
|
|
931
|
+
FROM messages m
|
|
932
|
+
${THREAD_JOIN}
|
|
933
|
+
WHERE m.thread_id = ?
|
|
841
934
|
ORDER BY m.created_at ASC, m.id ASC`)
|
|
842
935
|
.all(threadId);
|
|
843
936
|
const related = this.relatedFor(rows.map((r) => r.message_id));
|
|
@@ -846,8 +939,8 @@ export class MessageStore extends EventEmitter {
|
|
|
846
939
|
/** Message complet vu du serveur, avec l'état de lecture de chaque destinataire. */
|
|
847
940
|
getMessageAdmin(messageId) {
|
|
848
941
|
const row = this.db
|
|
849
|
-
.prepare(`SELECT m.id AS message_id, m.thread_id, t.title AS thread_title, m.sender_id, m.subject, m.body,
|
|
850
|
-
m.priority, m.in_reply_to, m.created_at, 0 AS starred, ${ADMIN_DELIVERY}
|
|
942
|
+
.prepare(`SELECT m.id AS message_id, m.thread_id, t.title AS thread_title, m.sender_id, m.subject, m.body,
|
|
943
|
+
m.priority, m.in_reply_to, m.created_at, 0 AS starred, ${ADMIN_DELIVERY}
|
|
851
944
|
FROM messages m ${THREAD_JOIN} WHERE m.id = ?`)
|
|
852
945
|
.get(messageId);
|
|
853
946
|
if (!row)
|
|
@@ -869,7 +962,7 @@ export class MessageStore extends EventEmitter {
|
|
|
869
962
|
/** Pièce jointe vue du serveur, quelle que soit la boîte. */
|
|
870
963
|
getAttachmentAdmin(attachmentId) {
|
|
871
964
|
const row = this.db
|
|
872
|
-
.prepare(`SELECT id AS attachment_id, message_id, filename, mime_type, size, data
|
|
965
|
+
.prepare(`SELECT id AS attachment_id, message_id, filename, mime_type, size, data
|
|
873
966
|
FROM attachments WHERE id = ?`)
|
|
874
967
|
.get(attachmentId);
|
|
875
968
|
if (!row)
|
|
@@ -881,9 +974,9 @@ export class MessageStore extends EventEmitter {
|
|
|
881
974
|
const threads = this.db.prepare("SELECT COUNT(DISTINCT thread_id) AS n FROM messages").get().n;
|
|
882
975
|
const sentByAgent = new Map(this.db.prepare("SELECT sender_id, COUNT(*) AS n FROM messages GROUP BY sender_id").all().map((r) => [r.sender_id, r.n]));
|
|
883
976
|
const receivedByAgent = new Map(this.db
|
|
884
|
-
.prepare(`SELECT agent_id,
|
|
885
|
-
COUNT(*) AS received,
|
|
886
|
-
SUM(CASE WHEN read_at IS NULL AND folder != 'trash' THEN 1 ELSE 0 END) AS unread
|
|
977
|
+
.prepare(`SELECT agent_id,
|
|
978
|
+
COUNT(*) AS received,
|
|
979
|
+
SUM(CASE WHEN read_at IS NULL AND folder != 'trash' THEN 1 ELSE 0 END) AS unread
|
|
887
980
|
FROM deliveries WHERE role != 'sender' GROUP BY agent_id`)
|
|
888
981
|
.all().map((r) => [r.agent_id, r]));
|
|
889
982
|
const agents = this.listAgents().map((agent) => ({
|
|
@@ -930,14 +1023,14 @@ export class MessageStore extends EventEmitter {
|
|
|
930
1023
|
return related;
|
|
931
1024
|
const placeholders = ids.map(() => "?").join(",");
|
|
932
1025
|
const recipients = this.db
|
|
933
|
-
.prepare(`SELECT message_id, agent_id FROM deliveries
|
|
1026
|
+
.prepare(`SELECT message_id, agent_id FROM deliveries
|
|
934
1027
|
WHERE message_id IN (${placeholders}) AND role = 'to'`)
|
|
935
1028
|
.all(...ids);
|
|
936
1029
|
for (const row of recipients)
|
|
937
1030
|
related.recipients.set(row.message_id, row.agent_id);
|
|
938
1031
|
const attachments = this.db
|
|
939
|
-
.prepare(`SELECT id AS attachment_id, message_id, filename, mime_type, size FROM attachments
|
|
940
|
-
WHERE message_id IN (${placeholders})
|
|
1032
|
+
.prepare(`SELECT id AS attachment_id, message_id, filename, mime_type, size FROM attachments
|
|
1033
|
+
WHERE message_id IN (${placeholders})
|
|
941
1034
|
ORDER BY message_id, position`)
|
|
942
1035
|
.all(...ids);
|
|
943
1036
|
for (const { message_id, ...info } of attachments) {
|
|
@@ -975,6 +1068,17 @@ export class MessageStore extends EventEmitter {
|
|
|
975
1068
|
this.db.exec("DELETE FROM threads WHERE NOT EXISTS (SELECT 1 FROM messages m WHERE m.thread_id = threads.id)");
|
|
976
1069
|
}
|
|
977
1070
|
}
|
|
1071
|
+
function toLink(row) {
|
|
1072
|
+
return {
|
|
1073
|
+
link_id: row.id,
|
|
1074
|
+
note: row.note,
|
|
1075
|
+
created_at: row.created_at,
|
|
1076
|
+
mailboxes: [
|
|
1077
|
+
{ mailbox: row.mailbox_a, seen_at: row.seen_by_a_at },
|
|
1078
|
+
{ mailbox: row.mailbox_b, seen_at: row.seen_by_b_at },
|
|
1079
|
+
],
|
|
1080
|
+
};
|
|
1081
|
+
}
|
|
978
1082
|
export function normalizeAgentId(id) {
|
|
979
1083
|
return String(id ?? "").trim();
|
|
980
1084
|
}
|