polygram 0.6.2 → 0.6.4
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/lib/db.js +18 -0
- package/migrations/007-attachments-table.sql +25 -7
- package/package.json +1 -1
- package/polygram.js +40 -2
package/lib/db.js
CHANGED
|
@@ -463,6 +463,24 @@ function wrap(db) {
|
|
|
463
463
|
return db.prepare(sql).all(...args);
|
|
464
464
|
},
|
|
465
465
|
|
|
466
|
+
// Re-FK every attachment whose (chat_id, msg_id) is in `msg_ids` over
|
|
467
|
+
// to a single primary message row. Used when the media-group buffer
|
|
468
|
+
// coalesces N Telegram messages (each carrying one photo of an album)
|
|
469
|
+
// into one synthetic turn — siblings were recorded under their own
|
|
470
|
+
// msg_ids by recordInbound, but Claude needs to see them all under
|
|
471
|
+
// the primary message so handleMessage's per-message attachment
|
|
472
|
+
// lookup returns the full album.
|
|
473
|
+
reassignAttachmentsToMessage({ chat_id, msg_ids, target_message_id }) {
|
|
474
|
+
if (!Array.isArray(msg_ids) || msg_ids.length === 0) return { changes: 0 };
|
|
475
|
+
const placeholders = msg_ids.map(() => '?').join(',');
|
|
476
|
+
return db.prepare(`
|
|
477
|
+
UPDATE attachments
|
|
478
|
+
SET message_id = ?, msg_id = (SELECT msg_id FROM messages WHERE id = ?)
|
|
479
|
+
WHERE chat_id = ? AND msg_id IN (${placeholders})
|
|
480
|
+
AND message_id != ?
|
|
481
|
+
`).run(target_message_id, target_message_id, String(chat_id), ...msg_ids, target_message_id);
|
|
482
|
+
},
|
|
483
|
+
|
|
466
484
|
// Look up the messages.id auto-pk for an inbound message. Used by
|
|
467
485
|
// recordInbound to FK attachments to the just-inserted message even
|
|
468
486
|
// when an ON-CONFLICT update happened (lastInsertRowid is 0 in that
|
|
@@ -64,6 +64,19 @@ CREATE INDEX IF NOT EXISTS idx_attachments_unique_id
|
|
|
64
64
|
-- mime_type, size, file_id, file_unique_id, and optionally transcription.
|
|
65
65
|
-- Pre-0.5.x rows may not have file_unique_id. We pull what's there and
|
|
66
66
|
-- leave the rest NULL.
|
|
67
|
+
--
|
|
68
|
+
-- Robustness: json_each() raises on malformed JSON or on a non-array
|
|
69
|
+
-- root, which would roll back the entire migration transaction (one bad
|
|
70
|
+
-- row blocks the upgrade). The `json_valid(...) AND json_type(...) =
|
|
71
|
+
-- 'array'` guards skip those rows so the rest still backfill. Rows
|
|
72
|
+
-- without a `file_id` are also skipped — the schema declares file_id
|
|
73
|
+
-- NOT NULL and we'd rather drop a corrupt entry than materialise a
|
|
74
|
+
-- permanently un-redownloadable row with file_id=''.
|
|
75
|
+
-- Pre-filter messages in a subquery so json_each() only runs on valid
|
|
76
|
+
-- JSON arrays. SQLite's json_each raises on malformed JSON or on a
|
|
77
|
+
-- non-array root, and that error rolls back the whole migration. The
|
|
78
|
+
-- subquery materialises only rows that pass json_valid + json_type
|
|
79
|
+
-- before the join expands them.
|
|
67
80
|
INSERT INTO attachments (
|
|
68
81
|
message_id, chat_id, msg_id, thread_id, bot_name,
|
|
69
82
|
file_id, file_unique_id, kind, name, mime_type, size_bytes,
|
|
@@ -71,7 +84,7 @@ INSERT INTO attachments (
|
|
|
71
84
|
)
|
|
72
85
|
SELECT
|
|
73
86
|
m.id, m.chat_id, m.msg_id, m.thread_id, m.bot_name,
|
|
74
|
-
|
|
87
|
+
json_extract(att.value, '$.file_id'),
|
|
75
88
|
json_extract(att.value, '$.file_unique_id'),
|
|
76
89
|
COALESCE(json_extract(att.value, '$.kind'), 'document'),
|
|
77
90
|
json_extract(att.value, '$.name'),
|
|
@@ -81,9 +94,14 @@ SELECT
|
|
|
81
94
|
'downloaded',
|
|
82
95
|
json_extract(att.value, '$.transcription.text'),
|
|
83
96
|
m.ts
|
|
84
|
-
FROM
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
97
|
+
FROM (
|
|
98
|
+
SELECT id, chat_id, msg_id, thread_id, bot_name, attachments_json, ts
|
|
99
|
+
FROM messages
|
|
100
|
+
WHERE direction = 'in'
|
|
101
|
+
AND attachments_json IS NOT NULL
|
|
102
|
+
AND json_valid(attachments_json) = 1
|
|
103
|
+
AND json_type(attachments_json) = 'array'
|
|
104
|
+
AND NOT EXISTS (SELECT 1 FROM attachments a WHERE a.message_id = id)
|
|
105
|
+
) m, json_each(m.attachments_json) att
|
|
106
|
+
WHERE json_extract(att.value, '$.file_id') IS NOT NULL
|
|
107
|
+
AND json_extract(att.value, '$.file_id') != '';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "polygram",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
4
4
|
"description": "Telegram daemon for Claude Code that preserves the OpenClaw per-chat session model. Migration path for OpenClaw users moving to Claude Code.",
|
|
5
5
|
"main": "lib/ipc-client.js",
|
|
6
6
|
"bin": {
|
package/polygram.js
CHANGED
|
@@ -208,7 +208,13 @@ function recordInbound(msg) {
|
|
|
208
208
|
const chatConfig = config.chats[chatId];
|
|
209
209
|
const ts = (msg.date || Math.floor(Date.now() / 1000)) * 1000;
|
|
210
210
|
|
|
211
|
-
|
|
211
|
+
// Atomic message + attachments write: all-or-nothing so a half-applied
|
|
212
|
+
// record can never leave a message row with zero (or partial) attachment
|
|
213
|
+
// rows that boot replay would silently treat as "no media." Wrapping in
|
|
214
|
+
// db.raw.transaction also collapses the message + N-attachment fsyncs
|
|
215
|
+
// into one commit (perf win for media groups: 7-attachment albums go
|
|
216
|
+
// from 8 sync writes to 1).
|
|
217
|
+
const writeInbound = db.raw.transaction(() => {
|
|
212
218
|
db.insertMessage({
|
|
213
219
|
chat_id: chatId,
|
|
214
220
|
thread_id: threadId,
|
|
@@ -231,6 +237,13 @@ function recordInbound(msg) {
|
|
|
231
237
|
// the upsert path; an explicit lookup is cheap and always correct.
|
|
232
238
|
const messageId = db.getInboundMessageId({ chat_id: chatId, msg_id: msg.message_id });
|
|
233
239
|
if (!messageId) return;
|
|
240
|
+
// Edit-safe insert: Telegram edited_message events re-fire
|
|
241
|
+
// recordInbound with the same (chat_id, msg_id). Telegram doesn't
|
|
242
|
+
// permit replacing media in an edit (only text/caption), so if rows
|
|
243
|
+
// already exist for this message_id they're correct as-is —
|
|
244
|
+
// re-inserting would (a) duplicate them, (b) reset download_status
|
|
245
|
+
// back to 'pending' and lose the local_path we already fetched.
|
|
246
|
+
if (db.getAttachmentsByMessage(messageId).length > 0) return;
|
|
234
247
|
for (const att of attachments) {
|
|
235
248
|
db.insertAttachment({
|
|
236
249
|
message_id: messageId,
|
|
@@ -247,7 +260,9 @@ function recordInbound(msg) {
|
|
|
247
260
|
ts,
|
|
248
261
|
});
|
|
249
262
|
}
|
|
250
|
-
}
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
dbWrite(() => writeInbound(), `insert inbound ${chatId}/${msg.message_id}`);
|
|
251
266
|
}
|
|
252
267
|
|
|
253
268
|
|
|
@@ -1930,6 +1945,29 @@ function createBot(token) {
|
|
|
1930
1945
|
// all-media-no-text groups.
|
|
1931
1946
|
const primary = messages.find((m) => m.text || m.caption) || messages[0];
|
|
1932
1947
|
const merged = messages.flatMap((m) => extractAttachments(m));
|
|
1948
|
+
|
|
1949
|
+
// 0.6.0 attachment-table regression fix: recordInbound (called per
|
|
1950
|
+
// sibling on bot.on('message')) inserted each photo's row under its
|
|
1951
|
+
// OWN msg_id. handleMessage looks up attachments via
|
|
1952
|
+
// getAttachmentsByMessage(primary.message_id) — which only returns
|
|
1953
|
+
// the primary's row. Without re-FK'ing the siblings we'd silently
|
|
1954
|
+
// drop N-1 of N photos in any album, exactly the umi-assistant bug
|
|
1955
|
+
// the user hit (saw 1 of 2 photos sent in a Telegram album).
|
|
1956
|
+
const chatId = String(primary.chat.id);
|
|
1957
|
+
const primaryDbId = db.getInboundMessageId({
|
|
1958
|
+
chat_id: chatId, msg_id: primary.message_id,
|
|
1959
|
+
});
|
|
1960
|
+
const siblingMsgIds = messages
|
|
1961
|
+
.filter((m) => m.message_id !== primary.message_id)
|
|
1962
|
+
.map((m) => m.message_id);
|
|
1963
|
+
if (primaryDbId && siblingMsgIds.length) {
|
|
1964
|
+
dbWrite(() => db.reassignAttachmentsToMessage({
|
|
1965
|
+
chat_id: chatId,
|
|
1966
|
+
msg_ids: siblingMsgIds,
|
|
1967
|
+
target_message_id: primaryDbId,
|
|
1968
|
+
}), 'reassign media-group sibling attachments');
|
|
1969
|
+
}
|
|
1970
|
+
|
|
1933
1971
|
const synthetic = { ...primary, _mergedAttachments: merged };
|
|
1934
1972
|
// Carry the primary's text verbatim (dispatchRegularMessage re-cleans
|
|
1935
1973
|
// the mention). Caption → text so downstream sees it uniformly.
|