negotium 0.1.12 → 0.1.13
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/dist/canonical-mcp-bridge.js +2 -3
- package/dist/canonical-mcp-bridge.js.map +1 -1
- package/dist/chunk-a9y2rwxf.js.map +1 -1
- package/dist/{chunk-j0kjscb7.js → chunk-s2gez3wg.js} +20 -2
- package/dist/{chunk-j0kjscb7.js.map → chunk-s2gez3wg.js.map} +2 -2
- package/dist/cron.js +49 -25
- package/dist/cron.js.map +15 -14
- package/dist/hosted-agent.js +3012 -17
- package/dist/hosted-agent.js.map +30 -3
- package/dist/main.js +31323 -36
- package/dist/main.js.map +198 -3
- package/dist/prompts.js +11 -8
- package/dist/prompts.js.map +4 -4
- package/dist/runtime/src/application/submit-user-message.ts +3 -0
- package/dist/runtime/src/index.ts +7 -0
- package/dist/runtime/src/mcp/runtime-spec.ts +3 -0
- package/dist/runtime/src/mcp/session-comm/server.ts +1 -1
- package/dist/runtime/src/platform/mcp-config.ts +13 -0
- package/dist/runtime/src/prompts/builders.ts +20 -6
- package/dist/runtime/src/query/active-rooms.ts +2 -0
- package/dist/runtime/src/runtime/delivery-ack.ts +112 -0
- package/dist/runtime/src/runtime/errors.ts +12 -12
- package/dist/runtime/src/runtime/turn-runner.ts +12 -0
- package/dist/runtime/src/storage/runtime-turn-requests.ts +1 -0
- package/dist/runtime/src/types/api.ts +2 -0
- package/dist/runtime/src/types.ts +2 -0
- package/dist/runtime/src/version.ts +1 -1
- package/dist/runtime-helpers.js.map +1 -1
- package/dist/storage.js.map +1 -1
- package/dist/types/packages/core/src/mcp/runtime-spec.d.ts +2 -0
- package/dist/types/packages/core/src/platform/mcp-config.d.ts +4 -0
- package/dist/types/packages/core/src/prompts/builders.d.ts +2 -0
- package/dist/types/packages/core/src/types/api.d.ts +2 -0
- package/dist/types/packages/core/src/types.d.ts +2 -0
- package/dist/types/packages/core/src/version.d.ts +1 -1
- package/dist/vault.js.map +1 -1
- package/package.json +1 -1
- package/dist/chunk-00q25k7p.js +0 -2387
- package/dist/chunk-00q25k7p.js.map +0 -26
- package/dist/chunk-03kv7mvj.js +0 -2152
- package/dist/chunk-03kv7mvj.js.map +0 -15
- package/dist/chunk-05pm51dh.js +0 -589
- package/dist/chunk-05pm51dh.js.map +0 -11
- package/dist/chunk-0eh16bk1.js +0 -2936
- package/dist/chunk-0eh16bk1.js.map +0 -20
- package/dist/chunk-0yfqyv88.js +0 -108
- package/dist/chunk-0yfqyv88.js.map +0 -10
- package/dist/chunk-1k7t7bnx.js +0 -97
- package/dist/chunk-1k7t7bnx.js.map +0 -10
- package/dist/chunk-3gkfr9vb.js +0 -431
- package/dist/chunk-3gkfr9vb.js.map +0 -11
- package/dist/chunk-5akd1gbj.js +0 -281
- package/dist/chunk-5akd1gbj.js.map +0 -10
- package/dist/chunk-7k19pcg8.js +0 -577
- package/dist/chunk-7k19pcg8.js.map +0 -14
- package/dist/chunk-81tbfbmt.js +0 -10
- package/dist/chunk-81tbfbmt.js.map +0 -10
- package/dist/chunk-82qhv1mq.js +0 -512
- package/dist/chunk-82qhv1mq.js.map +0 -13
- package/dist/chunk-8dy7e2a9.js +0 -103
- package/dist/chunk-8dy7e2a9.js.map +0 -10
- package/dist/chunk-8f2sqmrj.js +0 -1712
- package/dist/chunk-8f2sqmrj.js.map +0 -18
- package/dist/chunk-9hjeemfz.js +0 -372
- package/dist/chunk-9hjeemfz.js.map +0 -10
- package/dist/chunk-a4aa2ccv.js +0 -141
- package/dist/chunk-a4aa2ccv.js.map +0 -10
- package/dist/chunk-bsg215q3.js +0 -3536
- package/dist/chunk-bsg215q3.js.map +0 -20
- package/dist/chunk-bxnfsc7w.js +0 -58
- package/dist/chunk-bxnfsc7w.js.map +0 -10
- package/dist/chunk-c500xe9z.js +0 -33
- package/dist/chunk-c500xe9z.js.map +0 -10
- package/dist/chunk-cngrf7ab.js +0 -149
- package/dist/chunk-cngrf7ab.js.map +0 -10
- package/dist/chunk-d7tprf06.js +0 -53
- package/dist/chunk-d7tprf06.js.map +0 -10
- package/dist/chunk-ddnrmkrv.js +0 -61
- package/dist/chunk-ddnrmkrv.js.map +0 -10
- package/dist/chunk-edb07sjp.js +0 -567
- package/dist/chunk-edb07sjp.js.map +0 -14
- package/dist/chunk-ejcm6zx8.js +0 -591
- package/dist/chunk-ejcm6zx8.js.map +0 -11
- package/dist/chunk-ezs352qs.js +0 -237
- package/dist/chunk-ezs352qs.js.map +0 -11
- package/dist/chunk-f5xgprg1.js +0 -59
- package/dist/chunk-f5xgprg1.js.map +0 -10
- package/dist/chunk-fbtb3v0s.js +0 -1339
- package/dist/chunk-fbtb3v0s.js.map +0 -16
- package/dist/chunk-ffefbd3r.js +0 -17
- package/dist/chunk-ffefbd3r.js.map +0 -10
- package/dist/chunk-gaknyw04.js +0 -708
- package/dist/chunk-gaknyw04.js.map +0 -14
- package/dist/chunk-gxmny6tq.js +0 -1637
- package/dist/chunk-gxmny6tq.js.map +0 -15
- package/dist/chunk-h7bj5zwb.js +0 -3069
- package/dist/chunk-h7bj5zwb.js.map +0 -32
- package/dist/chunk-hp4g7wx7.js +0 -46
- package/dist/chunk-hp4g7wx7.js.map +0 -10
- package/dist/chunk-hwy3n31w.js +0 -21
- package/dist/chunk-hwy3n31w.js.map +0 -9
- package/dist/chunk-jzz91jwh.js +0 -70
- package/dist/chunk-jzz91jwh.js.map +0 -10
- package/dist/chunk-k778y16p.js +0 -153
- package/dist/chunk-k778y16p.js.map +0 -11
- package/dist/chunk-mh5mexsf.js +0 -147
- package/dist/chunk-mh5mexsf.js.map +0 -11
- package/dist/chunk-mnwaext5.js +0 -318
- package/dist/chunk-mnwaext5.js.map +0 -10
- package/dist/chunk-nkgs40ch.js +0 -23
- package/dist/chunk-nkgs40ch.js.map +0 -10
- package/dist/chunk-pz57vwsm.js +0 -250
- package/dist/chunk-pz57vwsm.js.map +0 -11
- package/dist/chunk-r6kdp7gn.js +0 -252
- package/dist/chunk-r6kdp7gn.js.map +0 -10
- package/dist/chunk-rkvrkcgx.js +0 -117
- package/dist/chunk-rkvrkcgx.js.map +0 -10
- package/dist/chunk-s74rdyqe.js +0 -117
- package/dist/chunk-s74rdyqe.js.map +0 -12
- package/dist/chunk-sa874j5c.js +0 -9
- package/dist/chunk-sa874j5c.js.map +0 -10
- package/dist/chunk-sffqdkvq.js +0 -122
- package/dist/chunk-sffqdkvq.js.map +0 -10
- package/dist/chunk-tcs9qjps.js +0 -64
- package/dist/chunk-tcs9qjps.js.map +0 -10
- package/dist/chunk-wx83ep8e.js +0 -131
- package/dist/chunk-wx83ep8e.js.map +0 -10
- package/dist/chunk-xag86ng3.js +0 -122
- package/dist/chunk-xag86ng3.js.map +0 -10
- package/dist/chunk-xfdkep71.js +0 -371
- package/dist/chunk-xfdkep71.js.map +0 -11
- package/dist/chunk-xhm1rfpt.js +0 -2907
- package/dist/chunk-xhm1rfpt.js.map +0 -24
- package/dist/chunk-xpcps28p.js +0 -2138
- package/dist/chunk-xpcps28p.js.map +0 -29
- package/dist/chunk-yb4rbev9.js +0 -214
- package/dist/chunk-yb4rbev9.js.map +0 -10
package/dist/chunk-03kv7mvj.js
DELETED
|
@@ -1,2152 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env bun
|
|
2
|
-
// @bun
|
|
3
|
-
import {
|
|
4
|
-
defineNegotiumAdapter
|
|
5
|
-
} from "./chunk-sa874j5c.js";
|
|
6
|
-
import {
|
|
7
|
-
startDefaultNode
|
|
8
|
-
} from "./chunk-gxmny6tq.js";
|
|
9
|
-
import"./chunk-edb07sjp.js";
|
|
10
|
-
import {
|
|
11
|
-
TopicValidationError,
|
|
12
|
-
executeVaultCommand,
|
|
13
|
-
isTranscriptionConfigured,
|
|
14
|
-
isVaultCommandLine,
|
|
15
|
-
onShutdown,
|
|
16
|
-
renderTurnFooter,
|
|
17
|
-
submitUserMessage,
|
|
18
|
-
topicService,
|
|
19
|
-
transcribeAudio,
|
|
20
|
-
waitForRequiredRuntimeProcessLease
|
|
21
|
-
} from "./chunk-h7bj5zwb.js";
|
|
22
|
-
import"./chunk-tcs9qjps.js";
|
|
23
|
-
import {
|
|
24
|
-
TopicArchiveRequiredError
|
|
25
|
-
} from "./chunk-7k19pcg8.js";
|
|
26
|
-
import"./chunk-8dy7e2a9.js";
|
|
27
|
-
import"./chunk-9hjeemfz.js";
|
|
28
|
-
import"./chunk-1k7t7bnx.js";
|
|
29
|
-
import {
|
|
30
|
-
composeAttachmentPrompt,
|
|
31
|
-
ingestAttachment,
|
|
32
|
-
startAiTurn
|
|
33
|
-
} from "./chunk-xhm1rfpt.js";
|
|
34
|
-
import {
|
|
35
|
-
TopicTitleConflictError
|
|
36
|
-
} from "./chunk-mnwaext5.js";
|
|
37
|
-
import"./chunk-xpcps28p.js";
|
|
38
|
-
import {
|
|
39
|
-
ensurePersonalGeneral,
|
|
40
|
-
runtimeBus
|
|
41
|
-
} from "./chunk-fbtb3v0s.js";
|
|
42
|
-
import {
|
|
43
|
-
getTopic,
|
|
44
|
-
getTopicByNameForUser,
|
|
45
|
-
isTopicVisible,
|
|
46
|
-
listTopics
|
|
47
|
-
} from "./chunk-ejcm6zx8.js";
|
|
48
|
-
import"./chunk-hp4g7wx7.js";
|
|
49
|
-
import"./chunk-s74rdyqe.js";
|
|
50
|
-
import"./chunk-k778y16p.js";
|
|
51
|
-
import"./chunk-xag86ng3.js";
|
|
52
|
-
import"./chunk-wx83ep8e.js";
|
|
53
|
-
import {
|
|
54
|
-
errMsg,
|
|
55
|
-
extractFileTagPaths,
|
|
56
|
-
isSensitivePath,
|
|
57
|
-
stripFileTags
|
|
58
|
-
} from "./chunk-00q25k7p.js";
|
|
59
|
-
import"./chunk-gaknyw04.js";
|
|
60
|
-
import"./chunk-mh5mexsf.js";
|
|
61
|
-
import {
|
|
62
|
-
DATA_DIR,
|
|
63
|
-
isAgentKind
|
|
64
|
-
} from "./chunk-ezs352qs.js";
|
|
65
|
-
import"./chunk-j0kjscb7.js";
|
|
66
|
-
import"./chunk-82qhv1mq.js";
|
|
67
|
-
import {
|
|
68
|
-
logger
|
|
69
|
-
} from "./chunk-ffefbd3r.js";
|
|
70
|
-
import"./chunk-81tbfbmt.js";
|
|
71
|
-
import"./chunk-sffqdkvq.js";
|
|
72
|
-
import"./chunk-f5xgprg1.js";
|
|
73
|
-
import"./chunk-hwy3n31w.js";
|
|
74
|
-
|
|
75
|
-
// ../../adapters/telegram/src/cli.ts
|
|
76
|
-
import TelegramBot from "node-telegram-bot-api";
|
|
77
|
-
|
|
78
|
-
// ../../adapters/telegram/src/adapter.ts
|
|
79
|
-
import { existsSync } from "fs";
|
|
80
|
-
|
|
81
|
-
// ../../packages/adapter-sdk/src/outbox.ts
|
|
82
|
-
function exponentialOutboxBackoff(attempts, policy) {
|
|
83
|
-
return Math.min(policy.baseDelayMs * 2 ** Math.max(0, attempts - 1), policy.maxDelayMs);
|
|
84
|
-
}
|
|
85
|
-
function createDurableOutboxWorker(options) {
|
|
86
|
-
const now = options.now ?? Date.now;
|
|
87
|
-
const schedule = options.schedule ?? ((callback, delayMs) => setTimeout(callback, delayMs));
|
|
88
|
-
const cancel = options.cancel ?? ((timer2) => clearTimeout(timer2));
|
|
89
|
-
let timer;
|
|
90
|
-
let started = false;
|
|
91
|
-
let stopped = false;
|
|
92
|
-
let flushing = null;
|
|
93
|
-
let pendingAcknowledgement = null;
|
|
94
|
-
const reportError = (error) => {
|
|
95
|
-
try {
|
|
96
|
-
options.onError?.(error);
|
|
97
|
-
} catch {}
|
|
98
|
-
};
|
|
99
|
-
const scheduleFlush = () => {
|
|
100
|
-
if (!started || stopped || timer !== undefined)
|
|
101
|
-
return;
|
|
102
|
-
timer = schedule(() => {
|
|
103
|
-
timer = undefined;
|
|
104
|
-
flush();
|
|
105
|
-
}, options.policy.pollMs);
|
|
106
|
-
};
|
|
107
|
-
const performFlush = async () => {
|
|
108
|
-
let entries = [];
|
|
109
|
-
const released = new Set;
|
|
110
|
-
const release = (entry) => {
|
|
111
|
-
if (released.has(entry))
|
|
112
|
-
return;
|
|
113
|
-
released.add(entry);
|
|
114
|
-
options.onEntryEnd?.(entry);
|
|
115
|
-
};
|
|
116
|
-
try {
|
|
117
|
-
if (pendingAcknowledgement) {
|
|
118
|
-
const pending = pendingAcknowledgement;
|
|
119
|
-
try {
|
|
120
|
-
await options.store.acknowledge(pending.entry, pending.delivery);
|
|
121
|
-
pendingAcknowledgement = null;
|
|
122
|
-
} catch (error) {
|
|
123
|
-
reportError(error);
|
|
124
|
-
return;
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
entries = await options.store.due(now());
|
|
128
|
-
for (const entry of entries)
|
|
129
|
-
options.onEntryStart?.(entry);
|
|
130
|
-
for (const entry of entries) {
|
|
131
|
-
if (stopped)
|
|
132
|
-
return;
|
|
133
|
-
if (await options.shouldDiscard?.(entry)) {
|
|
134
|
-
await options.store.discard(entry);
|
|
135
|
-
release(entry);
|
|
136
|
-
continue;
|
|
137
|
-
}
|
|
138
|
-
let delivery;
|
|
139
|
-
try {
|
|
140
|
-
delivery = await options.deliver(entry);
|
|
141
|
-
} catch (error) {
|
|
142
|
-
if (stopped)
|
|
143
|
-
return;
|
|
144
|
-
const failure = options.classifyError(error);
|
|
145
|
-
const attempts = entry.attempts + 1;
|
|
146
|
-
const retry = { attempts, error: failure.message };
|
|
147
|
-
if (attempts >= options.policy.maxAttempts) {
|
|
148
|
-
await options.store.deadLetter(entry, retry);
|
|
149
|
-
options.onDeadLetter?.(entry, retry);
|
|
150
|
-
} else {
|
|
151
|
-
const delay = failure.retryAfterMs ?? exponentialOutboxBackoff(attempts, options.policy);
|
|
152
|
-
await options.store.retry(entry, {
|
|
153
|
-
...retry,
|
|
154
|
-
nextTryAt: now() + Math.max(0, delay)
|
|
155
|
-
});
|
|
156
|
-
}
|
|
157
|
-
release(entry);
|
|
158
|
-
continue;
|
|
159
|
-
}
|
|
160
|
-
if (stopped)
|
|
161
|
-
return;
|
|
162
|
-
pendingAcknowledgement = { entry, delivery };
|
|
163
|
-
try {
|
|
164
|
-
await options.store.acknowledge(entry, delivery);
|
|
165
|
-
pendingAcknowledgement = null;
|
|
166
|
-
} catch (error) {
|
|
167
|
-
reportError(error);
|
|
168
|
-
release(entry);
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
release(entry);
|
|
172
|
-
}
|
|
173
|
-
} catch (error) {
|
|
174
|
-
if (!stopped)
|
|
175
|
-
reportError(error);
|
|
176
|
-
} finally {
|
|
177
|
-
for (const entry of entries)
|
|
178
|
-
release(entry);
|
|
179
|
-
if (!stopped && started) {
|
|
180
|
-
let hasPending = pendingAcknowledgement !== null;
|
|
181
|
-
if (!hasPending) {
|
|
182
|
-
try {
|
|
183
|
-
hasPending = await options.store.hasPending();
|
|
184
|
-
} catch (error) {
|
|
185
|
-
reportError(error);
|
|
186
|
-
hasPending = true;
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
if (hasPending)
|
|
190
|
-
scheduleFlush();
|
|
191
|
-
}
|
|
192
|
-
}
|
|
193
|
-
};
|
|
194
|
-
const flush = () => {
|
|
195
|
-
if (stopped)
|
|
196
|
-
return Promise.resolve();
|
|
197
|
-
if (flushing)
|
|
198
|
-
return flushing;
|
|
199
|
-
flushing = performFlush().finally(() => {
|
|
200
|
-
flushing = null;
|
|
201
|
-
});
|
|
202
|
-
return flushing;
|
|
203
|
-
};
|
|
204
|
-
return {
|
|
205
|
-
start() {
|
|
206
|
-
if (started || stopped)
|
|
207
|
-
return;
|
|
208
|
-
started = true;
|
|
209
|
-
scheduleFlush();
|
|
210
|
-
},
|
|
211
|
-
wake() {
|
|
212
|
-
scheduleFlush();
|
|
213
|
-
},
|
|
214
|
-
flush,
|
|
215
|
-
stop() {
|
|
216
|
-
if (stopped)
|
|
217
|
-
return;
|
|
218
|
-
stopped = true;
|
|
219
|
-
if (timer !== undefined)
|
|
220
|
-
cancel(timer);
|
|
221
|
-
timer = undefined;
|
|
222
|
-
}
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
// ../../adapters/telegram/src/mapping-store.ts
|
|
227
|
-
import { Database } from "bun:sqlite";
|
|
228
|
-
import { mkdirSync } from "fs";
|
|
229
|
-
import { dirname, join } from "path";
|
|
230
|
-
var NO_THREAD = 0;
|
|
231
|
-
function outboxRowToEntry(row) {
|
|
232
|
-
return {
|
|
233
|
-
id: row.id,
|
|
234
|
-
chatId: row.chat_id,
|
|
235
|
-
...row.thread_id !== NO_THREAD ? { threadId: row.thread_id } : {},
|
|
236
|
-
...row.runtime_message_id !== null ? { runtimeMessageId: row.runtime_message_id } : {},
|
|
237
|
-
...row.footer !== null ? { footer: row.footer } : {},
|
|
238
|
-
html: row.html,
|
|
239
|
-
plain: row.plain,
|
|
240
|
-
attempts: row.attempts,
|
|
241
|
-
nextTryAt: row.next_try_at,
|
|
242
|
-
dead: row.dead === 1,
|
|
243
|
-
...row.last_error !== null ? { lastError: row.last_error } : {}
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
function migrateLegacySchema(db) {
|
|
247
|
-
const existing = db.query("SELECT sql FROM sqlite_master WHERE type = 'table' AND name = 'mappings'").get();
|
|
248
|
-
if (!existing?.sql?.includes("UNIQUE (topic_id)"))
|
|
249
|
-
return;
|
|
250
|
-
db.run("DROP TABLE IF EXISTS mappings_legacy");
|
|
251
|
-
db.run("ALTER TABLE mappings RENAME TO mappings_legacy");
|
|
252
|
-
createTables(db);
|
|
253
|
-
db.run(`INSERT OR IGNORE INTO mappings (chat_id, thread_id, topic_id)
|
|
254
|
-
SELECT chat_id, thread_id, topic_id FROM mappings_legacy`);
|
|
255
|
-
db.run("DROP TABLE mappings_legacy");
|
|
256
|
-
}
|
|
257
|
-
function createTables(db) {
|
|
258
|
-
db.run(`CREATE TABLE IF NOT EXISTS mappings (
|
|
259
|
-
chat_id INTEGER NOT NULL,
|
|
260
|
-
thread_id INTEGER NOT NULL DEFAULT ${NO_THREAD},
|
|
261
|
-
topic_id TEXT NOT NULL,
|
|
262
|
-
UNIQUE (chat_id, thread_id)
|
|
263
|
-
)`);
|
|
264
|
-
db.run(`CREATE TABLE IF NOT EXISTS tombstones (
|
|
265
|
-
topic_id TEXT PRIMARY KEY,
|
|
266
|
-
title TEXT NOT NULL
|
|
267
|
-
)`);
|
|
268
|
-
db.run(`CREATE TABLE IF NOT EXISTS outbox (
|
|
269
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
270
|
-
chat_id INTEGER NOT NULL,
|
|
271
|
-
thread_id INTEGER NOT NULL DEFAULT ${NO_THREAD},
|
|
272
|
-
runtime_message_id TEXT,
|
|
273
|
-
footer TEXT,
|
|
274
|
-
html TEXT NOT NULL,
|
|
275
|
-
plain TEXT NOT NULL,
|
|
276
|
-
attempts INTEGER NOT NULL DEFAULT 0,
|
|
277
|
-
next_try_at INTEGER NOT NULL,
|
|
278
|
-
dead INTEGER NOT NULL DEFAULT 0,
|
|
279
|
-
last_error TEXT
|
|
280
|
-
)`);
|
|
281
|
-
db.run(`CREATE TABLE IF NOT EXISTS settings (
|
|
282
|
-
key TEXT PRIMARY KEY,
|
|
283
|
-
value TEXT NOT NULL
|
|
284
|
-
)`);
|
|
285
|
-
}
|
|
286
|
-
function migrateOutboxSchema(db) {
|
|
287
|
-
try {
|
|
288
|
-
db.run("ALTER TABLE outbox ADD COLUMN runtime_message_id TEXT");
|
|
289
|
-
} catch {}
|
|
290
|
-
try {
|
|
291
|
-
db.run("ALTER TABLE outbox ADD COLUMN footer TEXT");
|
|
292
|
-
} catch {}
|
|
293
|
-
db.run("CREATE INDEX IF NOT EXISTS idx_telegram_outbox_runtime_message ON outbox(runtime_message_id)");
|
|
294
|
-
}
|
|
295
|
-
function openMappingStore(path) {
|
|
296
|
-
const dbPath = path ?? join(DATA_DIR, "adapter-telegram.db");
|
|
297
|
-
if (dbPath !== ":memory:")
|
|
298
|
-
mkdirSync(dirname(dbPath), { recursive: true });
|
|
299
|
-
const db = new Database(dbPath);
|
|
300
|
-
migrateLegacySchema(db);
|
|
301
|
-
createTables(db);
|
|
302
|
-
migrateOutboxSchema(db);
|
|
303
|
-
return {
|
|
304
|
-
load() {
|
|
305
|
-
const rows = db.query("SELECT chat_id, thread_id, topic_id FROM mappings").all();
|
|
306
|
-
return rows.map((row) => ({
|
|
307
|
-
chatId: row.chat_id,
|
|
308
|
-
topicId: row.topic_id,
|
|
309
|
-
...row.thread_id !== NO_THREAD ? { threadId: row.thread_id } : {}
|
|
310
|
-
}));
|
|
311
|
-
},
|
|
312
|
-
save(mapping) {
|
|
313
|
-
db.run(`INSERT OR REPLACE INTO mappings (chat_id, thread_id, topic_id)
|
|
314
|
-
VALUES (?, ?, ?)`, [mapping.chatId, mapping.threadId ?? NO_THREAD, mapping.topicId]);
|
|
315
|
-
},
|
|
316
|
-
deleteByChat(chatId, threadId) {
|
|
317
|
-
db.run("DELETE FROM mappings WHERE chat_id = ? AND thread_id = ?", [
|
|
318
|
-
chatId,
|
|
319
|
-
threadId ?? NO_THREAD
|
|
320
|
-
]);
|
|
321
|
-
},
|
|
322
|
-
deleteByTopic(topicId) {
|
|
323
|
-
db.run("DELETE FROM mappings WHERE topic_id = ?", [topicId]);
|
|
324
|
-
},
|
|
325
|
-
loadTombstones() {
|
|
326
|
-
const rows = db.query("SELECT topic_id, title FROM tombstones").all();
|
|
327
|
-
return rows.map((row) => ({ topicId: row.topic_id, title: row.title }));
|
|
328
|
-
},
|
|
329
|
-
saveTombstone(topicId, title) {
|
|
330
|
-
db.run("INSERT OR REPLACE INTO tombstones (topic_id, title) VALUES (?, ?)", [topicId, title]);
|
|
331
|
-
},
|
|
332
|
-
deleteTombstone(topicId) {
|
|
333
|
-
db.run("DELETE FROM tombstones WHERE topic_id = ?", [topicId]);
|
|
334
|
-
},
|
|
335
|
-
clearTombstones() {
|
|
336
|
-
db.run("DELETE FROM tombstones");
|
|
337
|
-
},
|
|
338
|
-
loadForumChatId() {
|
|
339
|
-
const row = db.query("SELECT value FROM settings WHERE key = 'forum_chat_id'").get();
|
|
340
|
-
if (!row)
|
|
341
|
-
return;
|
|
342
|
-
const value = Number.parseInt(row.value, 10);
|
|
343
|
-
return Number.isSafeInteger(value) ? value : undefined;
|
|
344
|
-
},
|
|
345
|
-
saveForumChatId(chatId) {
|
|
346
|
-
db.run("INSERT OR REPLACE INTO settings (key, value) VALUES ('forum_chat_id', ?)", [
|
|
347
|
-
String(chatId)
|
|
348
|
-
]);
|
|
349
|
-
},
|
|
350
|
-
clearForumChatId() {
|
|
351
|
-
db.run("DELETE FROM settings WHERE key = 'forum_chat_id'");
|
|
352
|
-
},
|
|
353
|
-
outboxEnqueue(entry) {
|
|
354
|
-
db.run(`INSERT INTO outbox
|
|
355
|
-
(chat_id, thread_id, runtime_message_id, footer, html, plain, attempts, next_try_at, last_error)
|
|
356
|
-
VALUES (?, ?, ?, ?, ?, ?, 0, ?, ?)`, [
|
|
357
|
-
entry.chatId,
|
|
358
|
-
entry.threadId ?? NO_THREAD,
|
|
359
|
-
entry.runtimeMessageId ?? null,
|
|
360
|
-
entry.footer ?? null,
|
|
361
|
-
entry.html,
|
|
362
|
-
entry.plain,
|
|
363
|
-
entry.nextTryAt,
|
|
364
|
-
entry.lastError ?? null
|
|
365
|
-
]);
|
|
366
|
-
},
|
|
367
|
-
outboxDue(now) {
|
|
368
|
-
const rows = db.query("SELECT * FROM outbox WHERE dead = 0 AND next_try_at <= ? ORDER BY id ASC").all(now);
|
|
369
|
-
return rows.map(outboxRowToEntry);
|
|
370
|
-
},
|
|
371
|
-
outboxReschedule(id, attempts, nextTryAt, lastError) {
|
|
372
|
-
db.run("UPDATE outbox SET attempts = ?, next_try_at = ?, last_error = ? WHERE id = ?", [
|
|
373
|
-
attempts,
|
|
374
|
-
nextTryAt,
|
|
375
|
-
lastError,
|
|
376
|
-
id
|
|
377
|
-
]);
|
|
378
|
-
},
|
|
379
|
-
outboxMarkDead(id, attempts, lastError) {
|
|
380
|
-
db.run("UPDATE outbox SET dead = 1, attempts = ?, last_error = ? WHERE id = ?", [
|
|
381
|
-
attempts,
|
|
382
|
-
lastError,
|
|
383
|
-
id
|
|
384
|
-
]);
|
|
385
|
-
},
|
|
386
|
-
outboxDelete(id) {
|
|
387
|
-
db.run("DELETE FROM outbox WHERE id = ?", [id]);
|
|
388
|
-
},
|
|
389
|
-
outboxDeleteByChat(chatId) {
|
|
390
|
-
db.run("DELETE FROM outbox WHERE chat_id = ?", [chatId]);
|
|
391
|
-
},
|
|
392
|
-
outboxDeleteByRuntimeMessageId(runtimeMessageId) {
|
|
393
|
-
db.run("DELETE FROM outbox WHERE runtime_message_id = ?", [runtimeMessageId]);
|
|
394
|
-
},
|
|
395
|
-
outboxAll() {
|
|
396
|
-
const rows = db.query("SELECT * FROM outbox ORDER BY id ASC").all();
|
|
397
|
-
return rows.map(outboxRowToEntry);
|
|
398
|
-
},
|
|
399
|
-
close() {
|
|
400
|
-
db.close();
|
|
401
|
-
}
|
|
402
|
-
};
|
|
403
|
-
}
|
|
404
|
-
|
|
405
|
-
// ../../adapters/telegram/src/render.ts
|
|
406
|
-
function escapeHtml(text) {
|
|
407
|
-
return text.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
408
|
-
}
|
|
409
|
-
function markdownToTelegramHtml(md) {
|
|
410
|
-
const codeBlocks = [];
|
|
411
|
-
let text = md.replace(/```(\w*)\n([\s\S]*?)```/g, (_match, lang, code) => {
|
|
412
|
-
const escaped = escapeHtml(code.replace(/\n$/, ""));
|
|
413
|
-
const tag = lang ? `<pre><code class="language-${escapeHtml(lang)}">${escaped}</code></pre>` : `<pre>${escaped}</pre>`;
|
|
414
|
-
codeBlocks.push(tag);
|
|
415
|
-
return `\x00CODEBLOCK${codeBlocks.length - 1}\x00`;
|
|
416
|
-
});
|
|
417
|
-
const inlineCodes = [];
|
|
418
|
-
text = text.replace(/`([^`\n]+)`/g, (_match, code) => {
|
|
419
|
-
inlineCodes.push(`<code>${escapeHtml(code)}</code>`);
|
|
420
|
-
return `\x00INLINE${inlineCodes.length - 1}\x00`;
|
|
421
|
-
});
|
|
422
|
-
text = escapeHtml(text);
|
|
423
|
-
text = text.replace(/^#{1,6}\s+(.+)$/gm, "<b>$1</b>");
|
|
424
|
-
text = text.replace(/\*\*\*(.+?)\*\*\*/g, "<b><i>$1</i></b>");
|
|
425
|
-
text = text.replace(/\*\*(.+?)\*\*/g, "<b>$1</b>");
|
|
426
|
-
text = text.replace(/(?<!\w)\*([^\s*](?:.*?[^\s*])?)\*(?!\w)/g, "<i>$1</i>");
|
|
427
|
-
text = text.replace(/~~(.+?)~~/g, "<s>$1</s>");
|
|
428
|
-
text = text.replace(/\[([^\]]+)\]\(([^)]+)\)/g, '<a href="$2">$1</a>');
|
|
429
|
-
text = text.replace(/(?:^> .+\n?)+/gm, (block) => {
|
|
430
|
-
const content = block.replace(/^> /gm, "").trim();
|
|
431
|
-
return `<blockquote>${content}</blockquote>`;
|
|
432
|
-
});
|
|
433
|
-
text = text.replace(/\x00INLINE(\d+)\x00/g, (_m, i) => inlineCodes[Number(i)]);
|
|
434
|
-
text = text.replace(/\x00CODEBLOCK(\d+)\x00/g, (_m, i) => codeBlocks[Number(i)]);
|
|
435
|
-
return text;
|
|
436
|
-
}
|
|
437
|
-
function splitMessage(text, maxLen = 4096) {
|
|
438
|
-
if (text.length <= maxLen)
|
|
439
|
-
return [text];
|
|
440
|
-
const chunks = [];
|
|
441
|
-
let remaining = text;
|
|
442
|
-
while (remaining.length > 0) {
|
|
443
|
-
if (remaining.length <= maxLen) {
|
|
444
|
-
chunks.push(remaining);
|
|
445
|
-
break;
|
|
446
|
-
}
|
|
447
|
-
let splitAt = remaining.lastIndexOf(`
|
|
448
|
-
`, maxLen);
|
|
449
|
-
const atNewline = splitAt >= maxLen / 2;
|
|
450
|
-
if (!atNewline)
|
|
451
|
-
splitAt = maxLen;
|
|
452
|
-
chunks.push(remaining.slice(0, splitAt));
|
|
453
|
-
remaining = remaining.slice(atNewline ? splitAt + 1 : splitAt);
|
|
454
|
-
}
|
|
455
|
-
return chunks;
|
|
456
|
-
}
|
|
457
|
-
function renderOutbound(text, maxLen = 4096) {
|
|
458
|
-
return splitMessage(text, maxLen).map((plain) => ({
|
|
459
|
-
plain,
|
|
460
|
-
html: markdownToTelegramHtml(plain)
|
|
461
|
-
}));
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// ../../adapters/telegram/src/adapter.ts
|
|
465
|
-
var FORUM_TOPIC_NAME_MAX = 128;
|
|
466
|
-
var MAX_PARENT_HOPS = 5;
|
|
467
|
-
var DEFAULT_SEND_TIMEOUT_MS = 60000;
|
|
468
|
-
var PHOTO_EXTS = new Set(["png", "jpg", "jpeg", "gif", "webp"]);
|
|
469
|
-
function onboardingGuide(botUsername) {
|
|
470
|
-
const identity = botUsername ? `
|
|
471
|
-
Bot username: @${botUsername.replace(/^@/, "")}
|
|
472
|
-
` : `
|
|
473
|
-
`;
|
|
474
|
-
return "Welcome to Negotium. This DM is your General manager: chat naturally here to create, " + `delegate work to, stop, or delete topics.
|
|
475
|
-
|
|
476
|
-
` + `Connect a Telegram workspace:
|
|
477
|
-
` + `1. Create a supergroup and enable Topics.
|
|
478
|
-
` + `2. Add this bot to the group.
|
|
479
|
-
` + `3. Promote the bot to administrator and enable "Manage Topics".
|
|
480
|
-
` + `4. The group connects automatically; no /connect command is needed.
|
|
481
|
-
` + identity + "After connection, use the group's General topic as the manager and each forum topic as " + "an independent agent conversation.";
|
|
482
|
-
}
|
|
483
|
-
function isChatAdmin(member) {
|
|
484
|
-
return member.status === "administrator" || member.status === "creator";
|
|
485
|
-
}
|
|
486
|
-
function canManageTopics(member) {
|
|
487
|
-
return member.status === "creator" || member.status === "administrator" && member.can_manage_topics === true;
|
|
488
|
-
}
|
|
489
|
-
function isManageTopicsPermissionError(info) {
|
|
490
|
-
return /not enough rights|need administrator rights|chat_admin_required|manage topics/i.test(info.description);
|
|
491
|
-
}
|
|
492
|
-
function defaultTopicTitle(chatId, threadId) {
|
|
493
|
-
return threadId === undefined ? `tg-${chatId}` : `tg-${chatId}-${threadId}`;
|
|
494
|
-
}
|
|
495
|
-
function extractCommandArg(text) {
|
|
496
|
-
return text.split(/\s+/).slice(1).join(" ").trim();
|
|
497
|
-
}
|
|
498
|
-
function telegramErrorInfo(err) {
|
|
499
|
-
const e = err;
|
|
500
|
-
const status = typeof e?.response?.statusCode === "number" ? e.response.statusCode : undefined;
|
|
501
|
-
const description = [e?.response?.body?.description, e?.message].filter((s) => typeof s === "string").join(" ");
|
|
502
|
-
const retryRaw = e?.response?.body?.parameters?.retry_after;
|
|
503
|
-
return {
|
|
504
|
-
status,
|
|
505
|
-
...typeof e?.code === "string" ? { code: e.code } : {},
|
|
506
|
-
description,
|
|
507
|
-
...typeof retryRaw === "number" ? { retryAfterSec: retryRaw } : {}
|
|
508
|
-
};
|
|
509
|
-
}
|
|
510
|
-
function isHtmlParseError(info) {
|
|
511
|
-
if (info.status !== undefined && info.status !== 400)
|
|
512
|
-
return false;
|
|
513
|
-
return /can't parse entities/i.test(info.description);
|
|
514
|
-
}
|
|
515
|
-
function isForumTopicAlreadyGone(info) {
|
|
516
|
-
if (info.status !== undefined && info.status !== 400)
|
|
517
|
-
return false;
|
|
518
|
-
return /(?:message thread|forum topic).*not found|topic_id_invalid/i.test(info.description);
|
|
519
|
-
}
|
|
520
|
-
var RETRYABLE_CODES = new Set(["EFATAL", "ETIMEDOUT", "ECONNRESET", "ECONNREFUSED", "EAI_AGAIN"]);
|
|
521
|
-
function isRetryableSendError(info) {
|
|
522
|
-
if (info.status === 429)
|
|
523
|
-
return true;
|
|
524
|
-
if (info.status !== undefined && info.status >= 500 && info.status < 600)
|
|
525
|
-
return true;
|
|
526
|
-
return info.code !== undefined && RETRYABLE_CODES.has(info.code);
|
|
527
|
-
}
|
|
528
|
-
function startTelegramAdapter(opts) {
|
|
529
|
-
const { client, forumChatId } = opts;
|
|
530
|
-
const userId = opts.userId ?? "local";
|
|
531
|
-
const allowed = new Set((opts.allowedUsers ?? []).map((s) => s.trim()).filter(Boolean));
|
|
532
|
-
const configuredVaultOwner = opts.vaultOwnerTelegramUserId?.trim();
|
|
533
|
-
if (configuredVaultOwner && !allowed.has(configuredVaultOwner)) {
|
|
534
|
-
throw new Error("vaultOwnerTelegramUserId must appear in allowedUsers");
|
|
535
|
-
}
|
|
536
|
-
const vaultOwnerTelegramUserId = configuredVaultOwner || (allowed.size === 1 ? allowed.values().next().value : undefined);
|
|
537
|
-
const isAllowed = (telegramUserId) => allowed.size === 0 || allowed.has(String(telegramUserId));
|
|
538
|
-
const isVaultOwner = (telegramUserId) => vaultOwnerTelegramUserId !== undefined && String(telegramUserId) === vaultOwnerTelegramUserId;
|
|
539
|
-
const titleFor = opts.topicTitleFor ?? defaultTopicTitle;
|
|
540
|
-
const sendTimeoutMs = opts.sendTimeoutMs ?? DEFAULT_SEND_TIMEOUT_MS;
|
|
541
|
-
const typingHeartbeatMs = Math.max(250, opts.typingHeartbeatMs ?? 4000);
|
|
542
|
-
const footerEnabled = opts.footer === true;
|
|
543
|
-
const dispatchTurn = opts.startTurn ?? startAiTurn;
|
|
544
|
-
const store = openMappingStore(opts.mappingDbPath);
|
|
545
|
-
const restoredForumChatId = store.loadForumChatId();
|
|
546
|
-
const initialForumChatId = forumChatId ?? restoredForumChatId;
|
|
547
|
-
let forumChat = initialForumChatId ?? 0;
|
|
548
|
-
let forumMode = initialForumChatId !== undefined && typeof client.createForumTopic === "function";
|
|
549
|
-
let forumManageTopicsAvailable = forumMode && forumChatId !== undefined;
|
|
550
|
-
if (forumChatId !== undefined)
|
|
551
|
-
store.saveForumChatId(forumChatId);
|
|
552
|
-
if (initialForumChatId !== undefined && !forumMode) {
|
|
553
|
-
logger.warn({ forumChatId: initialForumChatId }, "telegram adapter: forumChatId set but client lacks createForumTopic \u2014 forum mode disabled");
|
|
554
|
-
}
|
|
555
|
-
const personalGeneral = ensurePersonalGeneral(userId);
|
|
556
|
-
const byKey = new Map;
|
|
557
|
-
const byTopic = new Map;
|
|
558
|
-
const targetByQueryId = new Map;
|
|
559
|
-
const typingHeartbeatByQueryId = new Map;
|
|
560
|
-
const toolStatusByQueryId = new Map;
|
|
561
|
-
const runtimeMessages = new Map;
|
|
562
|
-
const deliveredByRuntimeMessageId = new Map;
|
|
563
|
-
const deletedRuntimeMessageIds = new Set;
|
|
564
|
-
const activeRuntimeDeliveries = new Map;
|
|
565
|
-
const completedRuntimeMessageCleanup = new Set;
|
|
566
|
-
const forumCleanupInFlight = new Set;
|
|
567
|
-
const ownerDmChatIds = new Set([...allowed].map((value) => Number.parseInt(value, 10)).filter((value) => Number.isSafeInteger(value) && value > 0));
|
|
568
|
-
let stopped = false;
|
|
569
|
-
let botIdentity;
|
|
570
|
-
let botIdentityPromise;
|
|
571
|
-
function beginRuntimeDelivery(messageId) {
|
|
572
|
-
if (!messageId)
|
|
573
|
-
return;
|
|
574
|
-
activeRuntimeDeliveries.set(messageId, (activeRuntimeDeliveries.get(messageId) ?? 0) + 1);
|
|
575
|
-
}
|
|
576
|
-
function endRuntimeDelivery(messageId) {
|
|
577
|
-
if (!messageId)
|
|
578
|
-
return;
|
|
579
|
-
const remaining = (activeRuntimeDeliveries.get(messageId) ?? 1) - 1;
|
|
580
|
-
if (remaining > 0)
|
|
581
|
-
activeRuntimeDeliveries.set(messageId, remaining);
|
|
582
|
-
else {
|
|
583
|
-
activeRuntimeDeliveries.delete(messageId);
|
|
584
|
-
if (completedRuntimeMessageCleanup.delete(messageId)) {
|
|
585
|
-
deletedRuntimeMessageIds.delete(messageId);
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
function typingTargets(topicId, queryId) {
|
|
590
|
-
const target = queryId ? targetByQueryId.get(queryId) : undefined;
|
|
591
|
-
return target ? [target] : [...byTopic.get(topicId) ?? []];
|
|
592
|
-
}
|
|
593
|
-
function sendTyping(topicId, queryId) {
|
|
594
|
-
if (typeof client.sendChatAction !== "function")
|
|
595
|
-
return;
|
|
596
|
-
for (const mapping of typingTargets(topicId, queryId)) {
|
|
597
|
-
client.sendChatAction(mapping.chatId, "typing", threadOpts(mapping.threadId)).catch(() => {});
|
|
598
|
-
}
|
|
599
|
-
}
|
|
600
|
-
function stopTypingHeartbeat(queryId) {
|
|
601
|
-
const timer = typingHeartbeatByQueryId.get(queryId);
|
|
602
|
-
if (!timer)
|
|
603
|
-
return;
|
|
604
|
-
clearInterval(timer);
|
|
605
|
-
typingHeartbeatByQueryId.delete(queryId);
|
|
606
|
-
}
|
|
607
|
-
function startTypingHeartbeat(topicId, queryId) {
|
|
608
|
-
stopTypingHeartbeat(queryId);
|
|
609
|
-
sendTyping(topicId, queryId);
|
|
610
|
-
const timer = setInterval(() => {
|
|
611
|
-
if (stopped) {
|
|
612
|
-
stopTypingHeartbeat(queryId);
|
|
613
|
-
return;
|
|
614
|
-
}
|
|
615
|
-
sendTyping(topicId, queryId);
|
|
616
|
-
}, typingHeartbeatMs);
|
|
617
|
-
timer.unref?.();
|
|
618
|
-
typingHeartbeatByQueryId.set(queryId, timer);
|
|
619
|
-
}
|
|
620
|
-
function resolveBotIdentity() {
|
|
621
|
-
if (botIdentity)
|
|
622
|
-
return Promise.resolve(botIdentity);
|
|
623
|
-
if (typeof client.getMe !== "function")
|
|
624
|
-
return Promise.resolve(undefined);
|
|
625
|
-
if (botIdentityPromise)
|
|
626
|
-
return botIdentityPromise;
|
|
627
|
-
botIdentityPromise = (async () => {
|
|
628
|
-
let attempt = 0;
|
|
629
|
-
while (!stopped) {
|
|
630
|
-
try {
|
|
631
|
-
botIdentity = await client.getMe();
|
|
632
|
-
return botIdentity;
|
|
633
|
-
} catch (err) {
|
|
634
|
-
attempt += 1;
|
|
635
|
-
const waitMs = Math.min(5000, 250 * 2 ** Math.min(attempt - 1, 5));
|
|
636
|
-
logger.warn({ err, attempt, waitMs }, "telegram adapter: getMe failed while resolving onboarding identity; retrying");
|
|
637
|
-
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
638
|
-
}
|
|
639
|
-
}
|
|
640
|
-
return;
|
|
641
|
-
})().finally(() => {
|
|
642
|
-
botIdentityPromise = undefined;
|
|
643
|
-
});
|
|
644
|
-
return botIdentityPromise;
|
|
645
|
-
}
|
|
646
|
-
resolveBotIdentity();
|
|
647
|
-
const mappingKey = (chatId, threadId) => threadId === undefined ? String(chatId) : `${chatId}:${threadId}`;
|
|
648
|
-
const threadOpts = (threadId) => threadId === undefined ? {} : { message_thread_id: threadId };
|
|
649
|
-
function showToolStatus(topicId, queryId, label) {
|
|
650
|
-
const deleteMessage = client.deleteMessage?.bind(client);
|
|
651
|
-
if (!deleteMessage)
|
|
652
|
-
return;
|
|
653
|
-
let state = toolStatusByQueryId.get(queryId);
|
|
654
|
-
if (!state) {
|
|
655
|
-
state = { closed: false, refs: new Map, queue: Promise.resolve() };
|
|
656
|
-
toolStatusByQueryId.set(queryId, state);
|
|
657
|
-
}
|
|
658
|
-
const current = state;
|
|
659
|
-
const text = `\uD83D\uDD27 ${label}`.slice(0, 512);
|
|
660
|
-
current.queue = current.queue.then(async () => {
|
|
661
|
-
if (stopped || current.closed)
|
|
662
|
-
return;
|
|
663
|
-
for (const mapping of typingTargets(topicId, queryId)) {
|
|
664
|
-
if (stopped || current.closed)
|
|
665
|
-
return;
|
|
666
|
-
const key = mappingKey(mapping.chatId, mapping.threadId);
|
|
667
|
-
const existing = current.refs.get(key);
|
|
668
|
-
if (existing && typeof client.editMessageText === "function") {
|
|
669
|
-
try {
|
|
670
|
-
await client.editMessageText(text, {
|
|
671
|
-
chat_id: mapping.chatId,
|
|
672
|
-
message_id: existing.messageId
|
|
673
|
-
});
|
|
674
|
-
continue;
|
|
675
|
-
} catch (err) {
|
|
676
|
-
if (/message is not modified/i.test(telegramErrorInfo(err).description))
|
|
677
|
-
continue;
|
|
678
|
-
current.refs.delete(key);
|
|
679
|
-
await deleteMessage(mapping.chatId, existing.messageId).catch(() => {});
|
|
680
|
-
}
|
|
681
|
-
} else if (existing) {
|
|
682
|
-
current.refs.delete(key);
|
|
683
|
-
await deleteMessage(mapping.chatId, existing.messageId).catch(() => {});
|
|
684
|
-
}
|
|
685
|
-
try {
|
|
686
|
-
const sent = await client.sendMessage(mapping.chatId, text, threadOpts(mapping.threadId));
|
|
687
|
-
const messageId = sentMessageId(sent);
|
|
688
|
-
if (messageId === null)
|
|
689
|
-
continue;
|
|
690
|
-
if (stopped || current.closed) {
|
|
691
|
-
await deleteMessage(mapping.chatId, messageId).catch(() => {});
|
|
692
|
-
} else {
|
|
693
|
-
current.refs.set(key, { mapping, messageId });
|
|
694
|
-
}
|
|
695
|
-
} catch (err) {
|
|
696
|
-
logger.warn({ err, topicId, queryId, chatId: mapping.chatId }, "telegram adapter: tool status send failed");
|
|
697
|
-
}
|
|
698
|
-
}
|
|
699
|
-
if (!current.closed)
|
|
700
|
-
sendTyping(topicId, queryId);
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
function closeToolStatus(queryId) {
|
|
704
|
-
const state = toolStatusByQueryId.get(queryId);
|
|
705
|
-
if (!state)
|
|
706
|
-
return;
|
|
707
|
-
state.closed = true;
|
|
708
|
-
state.queue = state.queue.then(async () => {
|
|
709
|
-
if (typeof client.deleteMessage === "function") {
|
|
710
|
-
for (const { mapping, messageId } of state.refs.values()) {
|
|
711
|
-
await client.deleteMessage(mapping.chatId, messageId).catch(() => {});
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
state.refs.clear();
|
|
715
|
-
if (toolStatusByQueryId.get(queryId) === state)
|
|
716
|
-
toolStatusByQueryId.delete(queryId);
|
|
717
|
-
});
|
|
718
|
-
}
|
|
719
|
-
function detachFromTopic(mapping) {
|
|
720
|
-
const set = byTopic.get(mapping.topicId);
|
|
721
|
-
if (!set)
|
|
722
|
-
return;
|
|
723
|
-
set.delete(mapping);
|
|
724
|
-
if (set.size === 0)
|
|
725
|
-
byTopic.delete(mapping.topicId);
|
|
726
|
-
}
|
|
727
|
-
function bindMapping(chatId, threadId, topicId, { persist = true } = {}) {
|
|
728
|
-
const key = mappingKey(chatId, threadId);
|
|
729
|
-
const prev = byKey.get(key);
|
|
730
|
-
if (prev) {
|
|
731
|
-
if (prev.topicId === topicId)
|
|
732
|
-
return prev;
|
|
733
|
-
detachFromTopic(prev);
|
|
734
|
-
logger.info({ chatId, threadId, fromTopicId: prev.topicId, toTopicId: topicId }, "telegram adapter: re-binding chat/thread to a new topic");
|
|
735
|
-
}
|
|
736
|
-
const mapping = {
|
|
737
|
-
topicId,
|
|
738
|
-
chatId,
|
|
739
|
-
...threadId !== undefined ? { threadId } : {}
|
|
740
|
-
};
|
|
741
|
-
byKey.set(key, mapping);
|
|
742
|
-
let set = byTopic.get(topicId);
|
|
743
|
-
if (!set) {
|
|
744
|
-
set = new Set;
|
|
745
|
-
byTopic.set(topicId, set);
|
|
746
|
-
}
|
|
747
|
-
set.add(mapping);
|
|
748
|
-
if (persist)
|
|
749
|
-
store.save({ chatId, threadId, topicId });
|
|
750
|
-
return mapping;
|
|
751
|
-
}
|
|
752
|
-
function unbindTopic(topicId, { persist = true } = {}) {
|
|
753
|
-
const set = byTopic.get(topicId);
|
|
754
|
-
if (set) {
|
|
755
|
-
for (const mapping of set)
|
|
756
|
-
byKey.delete(mappingKey(mapping.chatId, mapping.threadId));
|
|
757
|
-
byTopic.delete(topicId);
|
|
758
|
-
}
|
|
759
|
-
if (persist)
|
|
760
|
-
store.deleteByTopic(topicId);
|
|
761
|
-
}
|
|
762
|
-
function cleanupPersistedMapping(mapping, logMessage) {
|
|
763
|
-
if (mapping.threadId !== undefined && typeof client.deleteForumTopic === "function") {
|
|
764
|
-
const key = mappingKey(mapping.chatId, mapping.threadId);
|
|
765
|
-
if (forumCleanupInFlight.has(key))
|
|
766
|
-
return;
|
|
767
|
-
forumCleanupInFlight.add(key);
|
|
768
|
-
client.deleteForumTopic(mapping.chatId, mapping.threadId).then(() => {
|
|
769
|
-
if (!stopped)
|
|
770
|
-
store.deleteByChat(mapping.chatId, mapping.threadId);
|
|
771
|
-
}).catch((err) => {
|
|
772
|
-
if (isForumTopicAlreadyGone(telegramErrorInfo(err))) {
|
|
773
|
-
if (!stopped)
|
|
774
|
-
store.deleteByChat(mapping.chatId, mapping.threadId);
|
|
775
|
-
return;
|
|
776
|
-
}
|
|
777
|
-
logger.warn({ err, topicId: mapping.topicId, threadId: mapping.threadId }, logMessage);
|
|
778
|
-
}).finally(() => forumCleanupInFlight.delete(key));
|
|
779
|
-
return;
|
|
780
|
-
}
|
|
781
|
-
store.deleteByChat(mapping.chatId, mapping.threadId);
|
|
782
|
-
}
|
|
783
|
-
function unloadMapping(chatId, threadId) {
|
|
784
|
-
const key = mappingKey(chatId, threadId);
|
|
785
|
-
const mapping = byKey.get(key);
|
|
786
|
-
if (!mapping)
|
|
787
|
-
return false;
|
|
788
|
-
detachFromTopic(mapping);
|
|
789
|
-
byKey.delete(key);
|
|
790
|
-
store.deleteByChat(chatId, threadId);
|
|
791
|
-
return true;
|
|
792
|
-
}
|
|
793
|
-
function loadExistingTopic(chatId, topicId, threadId) {
|
|
794
|
-
const topic = getTopic(topicId);
|
|
795
|
-
if (!topic?.participants.some((participant) => participant.userId === userId)) {
|
|
796
|
-
return false;
|
|
797
|
-
}
|
|
798
|
-
bindMapping(chatId, threadId, topic.id);
|
|
799
|
-
return true;
|
|
800
|
-
}
|
|
801
|
-
for (const persisted of store.load()) {
|
|
802
|
-
const topic = getTopic(persisted.topicId);
|
|
803
|
-
if (topic?.participants.some((participant) => participant.userId === userId)) {
|
|
804
|
-
bindMapping(persisted.chatId, persisted.threadId, persisted.topicId, { persist: false });
|
|
805
|
-
continue;
|
|
806
|
-
}
|
|
807
|
-
cleanupPersistedMapping(persisted, "telegram adapter: stale forum thread cleanup failed");
|
|
808
|
-
}
|
|
809
|
-
function reconcileStaleMappings() {
|
|
810
|
-
for (const persisted of store.load()) {
|
|
811
|
-
const topic = getTopic(persisted.topicId);
|
|
812
|
-
if (!topic?.participants.some((participant) => participant.userId === userId)) {
|
|
813
|
-
cleanupPersistedMapping(persisted, "telegram adapter: stale forum thread cleanup failed");
|
|
814
|
-
}
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
let suppressMaterialize = 0;
|
|
818
|
-
function registerTopicLocal(options) {
|
|
819
|
-
suppressMaterialize++;
|
|
820
|
-
try {
|
|
821
|
-
return topicService.create(options);
|
|
822
|
-
} finally {
|
|
823
|
-
suppressMaterialize--;
|
|
824
|
-
}
|
|
825
|
-
}
|
|
826
|
-
function getOrCreateTopic(title, agent) {
|
|
827
|
-
return getTopicByNameForUser(title, userId) ?? registerTopicLocal({ title, userId, kind: "agent", ...agent ? { agent } : {} });
|
|
828
|
-
}
|
|
829
|
-
function resolveMapping(chatId, threadId) {
|
|
830
|
-
const cached = byKey.get(mappingKey(chatId, threadId));
|
|
831
|
-
if (cached) {
|
|
832
|
-
const topic2 = getTopic(cached.topicId);
|
|
833
|
-
if (topic2)
|
|
834
|
-
return topic2;
|
|
835
|
-
unbindTopic(cached.topicId);
|
|
836
|
-
}
|
|
837
|
-
const topic = getOrCreateTopic(titleFor(chatId, threadId), opts.defaultAgent);
|
|
838
|
-
bindMapping(chatId, threadId, topic.id);
|
|
839
|
-
return topic;
|
|
840
|
-
}
|
|
841
|
-
const outboxCfg = {
|
|
842
|
-
pollMs: opts.outbox?.pollMs ?? 500,
|
|
843
|
-
baseDelayMs: opts.outbox?.baseDelayMs ?? 1000,
|
|
844
|
-
maxDelayMs: opts.outbox?.maxDelayMs ?? 60000,
|
|
845
|
-
maxAttempts: opts.outbox?.maxAttempts ?? 6
|
|
846
|
-
};
|
|
847
|
-
const outboxWorker = createDurableOutboxWorker({
|
|
848
|
-
policy: outboxCfg,
|
|
849
|
-
store: {
|
|
850
|
-
due: (now) => store.outboxDue(now),
|
|
851
|
-
hasPending: () => store.outboxAll().some((entry) => !entry.dead),
|
|
852
|
-
acknowledge: async (entry, delivery) => {
|
|
853
|
-
store.outboxDelete(entry.id);
|
|
854
|
-
const telegramMessageId = sentMessageId(delivery.sentValue);
|
|
855
|
-
if (!entry.runtimeMessageId || telegramMessageId === null)
|
|
856
|
-
return;
|
|
857
|
-
const ref = {
|
|
858
|
-
chatId: entry.chatId,
|
|
859
|
-
threadId: entry.threadId,
|
|
860
|
-
messageId: telegramMessageId,
|
|
861
|
-
kind: "text",
|
|
862
|
-
text: delivery.sentText,
|
|
863
|
-
html: delivery.sentAsHtml,
|
|
864
|
-
...entry.footer ? { footer: entry.footer } : {}
|
|
865
|
-
};
|
|
866
|
-
if (deletedRuntimeMessageIds.has(entry.runtimeMessageId)) {
|
|
867
|
-
await deleteDeliveredRefs(entry.runtimeMessageId, [ref]);
|
|
868
|
-
} else {
|
|
869
|
-
rememberDeliveredRefs(entry.runtimeMessageId, [ref]);
|
|
870
|
-
}
|
|
871
|
-
},
|
|
872
|
-
discard: (entry) => {
|
|
873
|
-
store.outboxDelete(entry.id);
|
|
874
|
-
},
|
|
875
|
-
retry: (entry, retry) => {
|
|
876
|
-
store.outboxReschedule(entry.id, retry.attempts, retry.nextTryAt, retry.error);
|
|
877
|
-
},
|
|
878
|
-
deadLetter: (entry, retry) => {
|
|
879
|
-
store.outboxMarkDead(entry.id, retry.attempts, retry.error);
|
|
880
|
-
}
|
|
881
|
-
},
|
|
882
|
-
shouldDiscard: (entry) => Boolean(entry.runtimeMessageId && deletedRuntimeMessageIds.has(entry.runtimeMessageId)),
|
|
883
|
-
deliver: async (entry) => {
|
|
884
|
-
try {
|
|
885
|
-
const sentValue = await client.sendMessage(entry.chatId, entry.html, {
|
|
886
|
-
...threadOpts(entry.threadId),
|
|
887
|
-
parse_mode: "HTML"
|
|
888
|
-
});
|
|
889
|
-
return { sentValue, sentText: entry.html, sentAsHtml: true };
|
|
890
|
-
} catch (error) {
|
|
891
|
-
if (!isHtmlParseError(telegramErrorInfo(error)))
|
|
892
|
-
throw error;
|
|
893
|
-
const sentValue = await client.sendMessage(entry.chatId, entry.plain, threadOpts(entry.threadId));
|
|
894
|
-
return { sentValue, sentText: entry.plain, sentAsHtml: false };
|
|
895
|
-
}
|
|
896
|
-
},
|
|
897
|
-
classifyError: (error) => {
|
|
898
|
-
const info = telegramErrorInfo(error);
|
|
899
|
-
return {
|
|
900
|
-
message: info.description || info.code || "send failed",
|
|
901
|
-
...info.status === 429 && info.retryAfterSec !== undefined ? { retryAfterMs: Math.max(info.retryAfterSec * 1000, 0) } : {}
|
|
902
|
-
};
|
|
903
|
-
},
|
|
904
|
-
onEntryStart: (entry) => beginRuntimeDelivery(entry.runtimeMessageId),
|
|
905
|
-
onEntryEnd: (entry) => endRuntimeDelivery(entry.runtimeMessageId),
|
|
906
|
-
onDeadLetter: (entry, retry) => {
|
|
907
|
-
logger.warn({ id: entry.id, chatId: entry.chatId, attempts: retry.attempts, error: retry.error }, "telegram adapter: outbox entry dead-lettered after max attempts");
|
|
908
|
-
},
|
|
909
|
-
onError: (error) => {
|
|
910
|
-
logger.warn({ err: error }, "telegram adapter: outbox flush failed");
|
|
911
|
-
}
|
|
912
|
-
});
|
|
913
|
-
outboxWorker.start();
|
|
914
|
-
function enqueueOutbox(chatId, threadId, html, plain, info, runtimeMessageId, footer) {
|
|
915
|
-
if (stopped)
|
|
916
|
-
return;
|
|
917
|
-
const initialDelay = info.status === 429 && info.retryAfterSec !== undefined ? info.retryAfterSec * 1000 : outboxCfg.baseDelayMs;
|
|
918
|
-
store.outboxEnqueue({
|
|
919
|
-
chatId,
|
|
920
|
-
threadId,
|
|
921
|
-
runtimeMessageId,
|
|
922
|
-
footer,
|
|
923
|
-
html,
|
|
924
|
-
plain,
|
|
925
|
-
nextTryAt: Date.now() + initialDelay,
|
|
926
|
-
lastError: info.description || info.code || "send failed"
|
|
927
|
-
});
|
|
928
|
-
logger.warn({ chatId, threadId, status: info.status, code: info.code }, "telegram adapter: transient send failure \u2014 queued for retry");
|
|
929
|
-
outboxWorker.wake();
|
|
930
|
-
}
|
|
931
|
-
function sentMessageId(value) {
|
|
932
|
-
if (!value || typeof value !== "object" || !("message_id" in value))
|
|
933
|
-
return null;
|
|
934
|
-
const id = value.message_id;
|
|
935
|
-
return typeof id === "number" && Number.isSafeInteger(id) ? id : null;
|
|
936
|
-
}
|
|
937
|
-
function rememberDeliveredRefs(messageId, refs) {
|
|
938
|
-
if (refs.length === 0)
|
|
939
|
-
return;
|
|
940
|
-
const current = deliveredByRuntimeMessageId.get(messageId) ?? [];
|
|
941
|
-
current.push(...refs);
|
|
942
|
-
deliveredByRuntimeMessageId.set(messageId, current);
|
|
943
|
-
}
|
|
944
|
-
async function deleteDeliveredRefs(runtimeMessageId, refs, topicId) {
|
|
945
|
-
if (typeof client.deleteMessage !== "function")
|
|
946
|
-
return;
|
|
947
|
-
for (const ref of refs) {
|
|
948
|
-
await client.deleteMessage(ref.chatId, ref.messageId).catch((err) => logger.warn({ err, topicId, messageId: runtimeMessageId, telegramMessageId: ref.messageId }, "telegram adapter: superseded message cleanup failed"));
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
function isDeliveredTextRef(ref) {
|
|
952
|
-
return ref.kind === "text" && typeof ref.text === "string" && typeof ref.html === "boolean";
|
|
953
|
-
}
|
|
954
|
-
async function deliver(chatId, threadId, text, runtimeMessageId, footer) {
|
|
955
|
-
const base = threadOpts(threadId);
|
|
956
|
-
const htmlOpts = { ...base, parse_mode: "HTML" };
|
|
957
|
-
const delivered = [];
|
|
958
|
-
const chunks = renderOutbound(text);
|
|
959
|
-
for (const [index, chunk] of chunks.entries()) {
|
|
960
|
-
const chunkFooter = footer && index === chunks.length - 1 ? footer : undefined;
|
|
961
|
-
try {
|
|
962
|
-
const sent = await client.sendMessage(chatId, chunk.html, htmlOpts);
|
|
963
|
-
const messageId = sentMessageId(sent);
|
|
964
|
-
if (messageId !== null) {
|
|
965
|
-
delivered.push({
|
|
966
|
-
messageId,
|
|
967
|
-
kind: "text",
|
|
968
|
-
text: chunk.html,
|
|
969
|
-
html: true,
|
|
970
|
-
...chunkFooter ? { footer: chunkFooter } : {}
|
|
971
|
-
});
|
|
972
|
-
}
|
|
973
|
-
} catch (err) {
|
|
974
|
-
const info = telegramErrorInfo(err);
|
|
975
|
-
if (isHtmlParseError(info)) {
|
|
976
|
-
try {
|
|
977
|
-
const sent = await client.sendMessage(chatId, chunk.plain, base);
|
|
978
|
-
const messageId = sentMessageId(sent);
|
|
979
|
-
if (messageId !== null) {
|
|
980
|
-
delivered.push({
|
|
981
|
-
messageId,
|
|
982
|
-
kind: "text",
|
|
983
|
-
text: chunk.plain,
|
|
984
|
-
html: false,
|
|
985
|
-
...chunkFooter ? { footer: chunkFooter } : {}
|
|
986
|
-
});
|
|
987
|
-
}
|
|
988
|
-
} catch (fallbackErr) {
|
|
989
|
-
const fallbackInfo = telegramErrorInfo(fallbackErr);
|
|
990
|
-
if (isRetryableSendError(fallbackInfo)) {
|
|
991
|
-
enqueueOutbox(chatId, threadId, chunk.plain, chunk.plain, fallbackInfo, runtimeMessageId, chunkFooter);
|
|
992
|
-
} else {
|
|
993
|
-
logger.warn({ err: fallbackErr, chatId }, "telegram adapter: plain fallback failed");
|
|
994
|
-
}
|
|
995
|
-
}
|
|
996
|
-
continue;
|
|
997
|
-
}
|
|
998
|
-
if (isRetryableSendError(info)) {
|
|
999
|
-
enqueueOutbox(chatId, threadId, chunk.html, chunk.plain, info, runtimeMessageId, chunkFooter);
|
|
1000
|
-
continue;
|
|
1001
|
-
}
|
|
1002
|
-
logger.warn({ err, chatId }, "telegram adapter: send failed \u2014 dropping chunk");
|
|
1003
|
-
}
|
|
1004
|
-
}
|
|
1005
|
-
return delivered;
|
|
1006
|
-
}
|
|
1007
|
-
async function sendFile(chatId, threadId, path) {
|
|
1008
|
-
const base = threadOpts(threadId);
|
|
1009
|
-
if (isSensitivePath(path)) {
|
|
1010
|
-
logger.warn({ path, chatId }, "telegram adapter: blocked sensitive file path");
|
|
1011
|
-
return null;
|
|
1012
|
-
}
|
|
1013
|
-
if (!existsSync(path)) {
|
|
1014
|
-
const sent = await client.sendMessage(chatId, `File: ${path}`, base).catch(() => null);
|
|
1015
|
-
const messageId = sentMessageId(sent);
|
|
1016
|
-
return messageId === null ? null : { messageId, kind: "media" };
|
|
1017
|
-
}
|
|
1018
|
-
const ext = path.split(".").pop()?.toLowerCase() ?? "";
|
|
1019
|
-
try {
|
|
1020
|
-
let sent;
|
|
1021
|
-
if (PHOTO_EXTS.has(ext) && typeof client.sendPhoto === "function") {
|
|
1022
|
-
sent = await client.sendPhoto(chatId, path, base);
|
|
1023
|
-
} else if (typeof client.sendDocument === "function") {
|
|
1024
|
-
sent = await client.sendDocument(chatId, path, base);
|
|
1025
|
-
} else {
|
|
1026
|
-
sent = await client.sendMessage(chatId, `File: ${path}`, base);
|
|
1027
|
-
}
|
|
1028
|
-
const messageId = sentMessageId(sent);
|
|
1029
|
-
return messageId === null ? null : { messageId, kind: "media" };
|
|
1030
|
-
} catch (err) {
|
|
1031
|
-
logger.warn({ err, path, chatId }, "telegram adapter: file send failed");
|
|
1032
|
-
return null;
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
async function deliverPayload(chatId, threadId, payload) {
|
|
1036
|
-
if (payload.runtimeMessageId && deletedRuntimeMessageIds.has(payload.runtimeMessageId))
|
|
1037
|
-
return;
|
|
1038
|
-
beginRuntimeDelivery(payload.runtimeMessageId);
|
|
1039
|
-
const deliveredRefs = [];
|
|
1040
|
-
try {
|
|
1041
|
-
let text = payload.text;
|
|
1042
|
-
let footer = null;
|
|
1043
|
-
if (payload.runtimeMessageId && footerEnabled) {
|
|
1044
|
-
const message = runtimeMessages.get(payload.runtimeMessageId);
|
|
1045
|
-
const shouldRenderFooter = message && (!message.queryId || message.usage);
|
|
1046
|
-
footer = shouldRenderFooter ? renderTurnFooter(message) : null;
|
|
1047
|
-
if (footer) {
|
|
1048
|
-
text = text ? `${text}
|
|
1049
|
-
|
|
1050
|
-
*${footer}*` : `*${footer}*`;
|
|
1051
|
-
}
|
|
1052
|
-
}
|
|
1053
|
-
if (text) {
|
|
1054
|
-
const delivered = await deliver(chatId, threadId, text, payload.runtimeMessageId, footer ?? undefined);
|
|
1055
|
-
deliveredRefs.push(...delivered.map((item) => ({
|
|
1056
|
-
...item,
|
|
1057
|
-
chatId,
|
|
1058
|
-
threadId
|
|
1059
|
-
})));
|
|
1060
|
-
}
|
|
1061
|
-
for (const path of payload.files) {
|
|
1062
|
-
if (payload.runtimeMessageId && deletedRuntimeMessageIds.has(payload.runtimeMessageId)) {
|
|
1063
|
-
break;
|
|
1064
|
-
}
|
|
1065
|
-
const delivered = await sendFile(chatId, threadId, path);
|
|
1066
|
-
if (delivered)
|
|
1067
|
-
deliveredRefs.push({ ...delivered, chatId, threadId });
|
|
1068
|
-
}
|
|
1069
|
-
if (payload.runtimeMessageId && deliveredRefs.length > 0) {
|
|
1070
|
-
if (deletedRuntimeMessageIds.has(payload.runtimeMessageId)) {
|
|
1071
|
-
await deleteDeliveredRefs(payload.runtimeMessageId, deliveredRefs);
|
|
1072
|
-
} else {
|
|
1073
|
-
rememberDeliveredRefs(payload.runtimeMessageId, deliveredRefs);
|
|
1074
|
-
}
|
|
1075
|
-
}
|
|
1076
|
-
} finally {
|
|
1077
|
-
endRuntimeDelivery(payload.runtimeMessageId);
|
|
1078
|
-
}
|
|
1079
|
-
}
|
|
1080
|
-
const sendQueues = new Map;
|
|
1081
|
-
function enqueueSend(topicId, task) {
|
|
1082
|
-
const prev = sendQueues.get(topicId) ?? Promise.resolve();
|
|
1083
|
-
const next = prev.then(() => new Promise((resolve) => {
|
|
1084
|
-
const timer = setTimeout(() => {
|
|
1085
|
-
logger.warn({ topicId, sendTimeoutMs }, "telegram adapter: send timed out \u2014 abandoning it and continuing the queue");
|
|
1086
|
-
resolve();
|
|
1087
|
-
}, sendTimeoutMs);
|
|
1088
|
-
task().catch((err) => logger.warn({ err, topicId }, "telegram adapter: send task failed")).finally(() => {
|
|
1089
|
-
clearTimeout(timer);
|
|
1090
|
-
resolve();
|
|
1091
|
-
});
|
|
1092
|
-
}));
|
|
1093
|
-
sendQueues.set(topicId, next);
|
|
1094
|
-
next.then(() => {
|
|
1095
|
-
if (sendQueues.get(topicId) === next)
|
|
1096
|
-
sendQueues.delete(topicId);
|
|
1097
|
-
});
|
|
1098
|
-
}
|
|
1099
|
-
function enqueueFanout(topicId, mappings, payload) {
|
|
1100
|
-
const targets = [...mappings];
|
|
1101
|
-
enqueueSend(topicId, async () => {
|
|
1102
|
-
for (const m of targets)
|
|
1103
|
-
await deliverPayload(m.chatId, m.threadId, payload);
|
|
1104
|
-
});
|
|
1105
|
-
}
|
|
1106
|
-
function enqueueTarget(topicId, target, payload) {
|
|
1107
|
-
enqueueSend(topicId, () => deliverPayload(target.chatId, target.threadId, payload));
|
|
1108
|
-
}
|
|
1109
|
-
const pendingByTopic = new Map;
|
|
1110
|
-
const permissionBlockedTopics = new Map;
|
|
1111
|
-
const materializeTombstones = new Map;
|
|
1112
|
-
if (forumMode) {
|
|
1113
|
-
for (const t of store.loadTombstones())
|
|
1114
|
-
materializeTombstones.set(t.topicId, t.title);
|
|
1115
|
-
}
|
|
1116
|
-
function deliverFallback(title, payload) {
|
|
1117
|
-
return deliverPayload(forumChat, undefined, {
|
|
1118
|
-
text: payload.text ? `[${title}] ${payload.text}` : "",
|
|
1119
|
-
files: payload.files,
|
|
1120
|
-
runtimeMessageId: payload.runtimeMessageId
|
|
1121
|
-
});
|
|
1122
|
-
}
|
|
1123
|
-
function flushBuffered(topicId, buffer, send) {
|
|
1124
|
-
for (const payload of buffer)
|
|
1125
|
-
enqueueSend(topicId, () => send(payload));
|
|
1126
|
-
}
|
|
1127
|
-
function materializeTopic(topic) {
|
|
1128
|
-
if (!forumManageTopicsAvailable || suppressMaterialize > 0 || byTopic.has(topic.id) || pendingByTopic.has(topic.id) || permissionBlockedTopics.has(topic.id) || materializeTombstones.has(topic.id)) {
|
|
1129
|
-
return;
|
|
1130
|
-
}
|
|
1131
|
-
if (!isTopicVisible(topic))
|
|
1132
|
-
return;
|
|
1133
|
-
if (!topic.participants?.some((p) => p.userId === userId))
|
|
1134
|
-
return;
|
|
1135
|
-
const materializationChat = forumChat;
|
|
1136
|
-
const pending = { buffer: [], cancelled: false };
|
|
1137
|
-
pendingByTopic.set(topic.id, pending);
|
|
1138
|
-
(async () => {
|
|
1139
|
-
let created;
|
|
1140
|
-
try {
|
|
1141
|
-
created = await client.createForumTopic(materializationChat, topic.title.slice(0, FORUM_TOPIC_NAME_MAX));
|
|
1142
|
-
} catch (err) {
|
|
1143
|
-
if (pendingByTopic.get(topic.id) === pending)
|
|
1144
|
-
pendingByTopic.delete(topic.id);
|
|
1145
|
-
if (stopped || pending.cancelled)
|
|
1146
|
-
return;
|
|
1147
|
-
const errorInfo = telegramErrorInfo(err);
|
|
1148
|
-
if (isManageTopicsPermissionError(errorInfo)) {
|
|
1149
|
-
const permissionWasAvailable = forumManageTopicsAvailable;
|
|
1150
|
-
forumManageTopicsAvailable = false;
|
|
1151
|
-
permissionBlockedTopics.set(topic.id, topic.title);
|
|
1152
|
-
logger.warn({ err, topicId: topic.id, title: topic.title }, "telegram adapter: Manage Topics unavailable \u2014 waiting for permission recovery");
|
|
1153
|
-
if (permissionWasAvailable) {
|
|
1154
|
-
reply(forumChat, undefined, 'Forum topic creation is paused. Enable the bot administrator permission "Manage Topics"; pending topics will be retried automatically.');
|
|
1155
|
-
}
|
|
1156
|
-
flushBuffered(topic.id, pending.buffer, (payload) => deliverFallback(topic.title, payload));
|
|
1157
|
-
return;
|
|
1158
|
-
}
|
|
1159
|
-
logger.warn({ err, topicId: topic.id, title: topic.title }, "telegram adapter: createForumTopic failed permanently \u2014 falling back to general chat");
|
|
1160
|
-
materializeTombstones.set(topic.id, topic.title);
|
|
1161
|
-
store.saveTombstone(topic.id, topic.title);
|
|
1162
|
-
flushBuffered(topic.id, pending.buffer, (payload) => deliverFallback(topic.title, payload));
|
|
1163
|
-
return;
|
|
1164
|
-
}
|
|
1165
|
-
if (pendingByTopic.get(topic.id) === pending)
|
|
1166
|
-
pendingByTopic.delete(topic.id);
|
|
1167
|
-
const threadId = created.message_thread_id;
|
|
1168
|
-
if (stopped || pending.cancelled) {
|
|
1169
|
-
if (pending.cancelled && typeof client.deleteForumTopic === "function") {
|
|
1170
|
-
const orphan = { chatId: materializationChat, threadId, topicId: topic.id };
|
|
1171
|
-
store.save(orphan);
|
|
1172
|
-
cleanupPersistedMapping(orphan, "telegram adapter: orphan thread cleanup failed");
|
|
1173
|
-
}
|
|
1174
|
-
return;
|
|
1175
|
-
}
|
|
1176
|
-
bindMapping(materializationChat, threadId, topic.id);
|
|
1177
|
-
flushBuffered(topic.id, pending.buffer, (payload) => deliverPayload(materializationChat, threadId, payload));
|
|
1178
|
-
})();
|
|
1179
|
-
}
|
|
1180
|
-
function handleTopicDeleted(topicId) {
|
|
1181
|
-
if (materializeTombstones.delete(topicId))
|
|
1182
|
-
store.deleteTombstone(topicId);
|
|
1183
|
-
const pending = pendingByTopic.get(topicId);
|
|
1184
|
-
if (pending)
|
|
1185
|
-
pending.cancelled = true;
|
|
1186
|
-
for (const [messageId, message] of runtimeMessages) {
|
|
1187
|
-
if (message.topicId === topicId)
|
|
1188
|
-
deleteDeliveredRuntimeMessage(topicId, messageId);
|
|
1189
|
-
}
|
|
1190
|
-
const set = byTopic.get(topicId);
|
|
1191
|
-
if (!set)
|
|
1192
|
-
return;
|
|
1193
|
-
const mappings = [...set];
|
|
1194
|
-
unbindTopic(topicId, { persist: false });
|
|
1195
|
-
for (const mapping of mappings) {
|
|
1196
|
-
cleanupPersistedMapping(mapping, "telegram adapter: deleteForumTopic failed");
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
function findMappedAncestor(topic) {
|
|
1200
|
-
const seen = new Set([topic.id]);
|
|
1201
|
-
let current = topic;
|
|
1202
|
-
for (let hop = 0;hop < MAX_PARENT_HOPS && current; hop++) {
|
|
1203
|
-
const parentId = current.parentTopicId;
|
|
1204
|
-
if (!parentId || seen.has(parentId))
|
|
1205
|
-
return;
|
|
1206
|
-
seen.add(parentId);
|
|
1207
|
-
const mappings = byTopic.get(parentId);
|
|
1208
|
-
if (mappings && mappings.size > 0)
|
|
1209
|
-
return mappings;
|
|
1210
|
-
current = getTopic(parentId);
|
|
1211
|
-
}
|
|
1212
|
-
return;
|
|
1213
|
-
}
|
|
1214
|
-
function routeMessage(topicId, payload, queryId) {
|
|
1215
|
-
const pending = pendingByTopic.get(topicId);
|
|
1216
|
-
if (pending) {
|
|
1217
|
-
pending.buffer.push(payload);
|
|
1218
|
-
return;
|
|
1219
|
-
}
|
|
1220
|
-
const specificTarget = queryId ? targetByQueryId.get(queryId) : undefined;
|
|
1221
|
-
if (specificTarget) {
|
|
1222
|
-
enqueueTarget(topicId, specificTarget, payload);
|
|
1223
|
-
return;
|
|
1224
|
-
}
|
|
1225
|
-
const mappings = byTopic.get(topicId);
|
|
1226
|
-
if (mappings && mappings.size > 0) {
|
|
1227
|
-
enqueueFanout(topicId, mappings, payload);
|
|
1228
|
-
return;
|
|
1229
|
-
}
|
|
1230
|
-
const tombstoneTitle = materializeTombstones.get(topicId);
|
|
1231
|
-
if (tombstoneTitle !== undefined) {
|
|
1232
|
-
enqueueSend(topicId, () => deliverFallback(tombstoneTitle, payload));
|
|
1233
|
-
return;
|
|
1234
|
-
}
|
|
1235
|
-
const topic = getTopic(topicId);
|
|
1236
|
-
if (!topic)
|
|
1237
|
-
return;
|
|
1238
|
-
if (forumMode) {
|
|
1239
|
-
if (!forumManageTopicsAvailable || permissionBlockedTopics.has(topicId)) {
|
|
1240
|
-
enqueueSend(topicId, () => deliverFallback(topic.title, payload));
|
|
1241
|
-
return;
|
|
1242
|
-
}
|
|
1243
|
-
materializeTopic(topic);
|
|
1244
|
-
pendingByTopic.get(topicId)?.buffer.push(payload);
|
|
1245
|
-
return;
|
|
1246
|
-
}
|
|
1247
|
-
const ancestorMappings = findMappedAncestor(topic);
|
|
1248
|
-
if (ancestorMappings) {
|
|
1249
|
-
enqueueFanout(topicId, ancestorMappings, {
|
|
1250
|
-
text: payload.text ? `[${topic.title}] ${payload.text}` : "",
|
|
1251
|
-
files: payload.files,
|
|
1252
|
-
runtimeMessageId: payload.runtimeMessageId
|
|
1253
|
-
});
|
|
1254
|
-
}
|
|
1255
|
-
}
|
|
1256
|
-
function deleteDeliveredRuntimeMessage(topicId, messageId) {
|
|
1257
|
-
deletedRuntimeMessageIds.add(messageId);
|
|
1258
|
-
completedRuntimeMessageCleanup.delete(messageId);
|
|
1259
|
-
runtimeMessages.delete(messageId);
|
|
1260
|
-
const pending = pendingByTopic.get(topicId);
|
|
1261
|
-
if (pending) {
|
|
1262
|
-
pending.buffer = pending.buffer.filter((payload) => payload.runtimeMessageId !== messageId);
|
|
1263
|
-
}
|
|
1264
|
-
store.outboxDeleteByRuntimeMessageId(messageId);
|
|
1265
|
-
enqueueSend(topicId, async () => {
|
|
1266
|
-
const refs = deliveredByRuntimeMessageId.get(messageId) ?? [];
|
|
1267
|
-
deliveredByRuntimeMessageId.delete(messageId);
|
|
1268
|
-
await deleteDeliveredRefs(messageId, refs, topicId);
|
|
1269
|
-
if ((activeRuntimeDeliveries.get(messageId) ?? 0) > 0) {
|
|
1270
|
-
completedRuntimeMessageCleanup.add(messageId);
|
|
1271
|
-
} else {
|
|
1272
|
-
completedRuntimeMessageCleanup.delete(messageId);
|
|
1273
|
-
deletedRuntimeMessageIds.delete(messageId);
|
|
1274
|
-
}
|
|
1275
|
-
});
|
|
1276
|
-
}
|
|
1277
|
-
function attachUpdatedFooter(topicId, messageId, footer) {
|
|
1278
|
-
enqueueSend(topicId, async () => {
|
|
1279
|
-
if (deletedRuntimeMessageIds.has(messageId))
|
|
1280
|
-
return;
|
|
1281
|
-
const refs = deliveredByRuntimeMessageId.get(messageId) ?? [];
|
|
1282
|
-
const refsByTarget = new Map;
|
|
1283
|
-
for (const ref of refs) {
|
|
1284
|
-
if (!isDeliveredTextRef(ref))
|
|
1285
|
-
continue;
|
|
1286
|
-
const key = `${ref.chatId}:${ref.threadId ?? "root"}`;
|
|
1287
|
-
const targetRefs = refsByTarget.get(key) ?? [];
|
|
1288
|
-
targetRefs.push(ref);
|
|
1289
|
-
refsByTarget.set(key, targetRefs);
|
|
1290
|
-
}
|
|
1291
|
-
const footerChunk = renderOutbound(`*${footer}*`)[0];
|
|
1292
|
-
if (!footerChunk)
|
|
1293
|
-
return;
|
|
1294
|
-
for (const targetRefs of refsByTarget.values()) {
|
|
1295
|
-
const ref = targetRefs.findLast((candidate) => candidate.footer !== undefined) ?? targetRefs.at(-1);
|
|
1296
|
-
if (!ref)
|
|
1297
|
-
continue;
|
|
1298
|
-
if (ref.footer === footer)
|
|
1299
|
-
continue;
|
|
1300
|
-
if (typeof client.editMessageText === "function") {
|
|
1301
|
-
const suffix = ref.html ? footerChunk.html : footerChunk.plain;
|
|
1302
|
-
const previousFooterChunk = ref.footer ? renderOutbound(`*${ref.footer}*`)[0] : undefined;
|
|
1303
|
-
const previousSuffix = previousFooterChunk ? ref.html ? previousFooterChunk.html : previousFooterChunk.plain : "";
|
|
1304
|
-
const previousTrailer = previousSuffix ? `
|
|
1305
|
-
|
|
1306
|
-
${previousSuffix}` : "";
|
|
1307
|
-
const baseText = previousTrailer && ref.text.endsWith(previousTrailer) ? ref.text.slice(0, -previousTrailer.length) : ref.text;
|
|
1308
|
-
const text = `${baseText}
|
|
1309
|
-
|
|
1310
|
-
${suffix}`;
|
|
1311
|
-
const editOptions = {
|
|
1312
|
-
chat_id: ref.chatId,
|
|
1313
|
-
message_id: ref.messageId,
|
|
1314
|
-
...ref.html ? { parse_mode: "HTML" } : {}
|
|
1315
|
-
};
|
|
1316
|
-
try {
|
|
1317
|
-
await client.editMessageText(text, editOptions);
|
|
1318
|
-
ref.text = text;
|
|
1319
|
-
ref.footer = footer;
|
|
1320
|
-
continue;
|
|
1321
|
-
} catch (err) {
|
|
1322
|
-
let editError = err;
|
|
1323
|
-
const info = telegramErrorInfo(err);
|
|
1324
|
-
if (isRetryableSendError(info)) {
|
|
1325
|
-
const retryDelayMs = Math.max(0, (info.retryAfterSec ?? 0) * 1000);
|
|
1326
|
-
if (retryDelayMs > 30000) {
|
|
1327
|
-
logger.warn({ err, topicId, messageId, retryDelayMs }, "telegram adapter: footer edit rate-limited; leaving footer unchanged");
|
|
1328
|
-
continue;
|
|
1329
|
-
}
|
|
1330
|
-
if (retryDelayMs > 0) {
|
|
1331
|
-
await new Promise((resolve) => setTimeout(resolve, retryDelayMs));
|
|
1332
|
-
}
|
|
1333
|
-
try {
|
|
1334
|
-
await client.editMessageText(text, editOptions);
|
|
1335
|
-
ref.text = text;
|
|
1336
|
-
ref.footer = footer;
|
|
1337
|
-
continue;
|
|
1338
|
-
} catch (retryErr) {
|
|
1339
|
-
if (isRetryableSendError(telegramErrorInfo(retryErr))) {
|
|
1340
|
-
logger.warn({ err: retryErr, topicId, messageId, telegramMessageId: ref.messageId }, "telegram adapter: footer edit retry failed; leaving footer unchanged");
|
|
1341
|
-
continue;
|
|
1342
|
-
}
|
|
1343
|
-
editError = retryErr;
|
|
1344
|
-
}
|
|
1345
|
-
}
|
|
1346
|
-
logger.warn({ err: editError, topicId, messageId, telegramMessageId: ref.messageId }, "telegram adapter: footer edit failed permanently; sending footer separately");
|
|
1347
|
-
}
|
|
1348
|
-
}
|
|
1349
|
-
const delivered = await deliver(ref.chatId, ref.threadId, `*${footer}*`, messageId, footer);
|
|
1350
|
-
refs.push(...delivered.map((item) => ({
|
|
1351
|
-
...item,
|
|
1352
|
-
chatId: ref.chatId,
|
|
1353
|
-
threadId: ref.threadId,
|
|
1354
|
-
footer
|
|
1355
|
-
})));
|
|
1356
|
-
}
|
|
1357
|
-
deliveredByRuntimeMessageId.set(messageId, refs);
|
|
1358
|
-
});
|
|
1359
|
-
}
|
|
1360
|
-
function clearQueryDeliveryState(queryId) {
|
|
1361
|
-
const timer = setTimeout(() => {
|
|
1362
|
-
for (const [messageId, message] of runtimeMessages) {
|
|
1363
|
-
if (message.queryId !== queryId)
|
|
1364
|
-
continue;
|
|
1365
|
-
runtimeMessages.delete(messageId);
|
|
1366
|
-
deliveredByRuntimeMessageId.delete(messageId);
|
|
1367
|
-
deletedRuntimeMessageIds.delete(messageId);
|
|
1368
|
-
}
|
|
1369
|
-
}, 5 * 60000);
|
|
1370
|
-
timer.unref?.();
|
|
1371
|
-
}
|
|
1372
|
-
function reply(chatId, threadId, text) {
|
|
1373
|
-
client.sendMessage(chatId, text, threadOpts(threadId)).catch((err) => logger.warn({ err, chatId }, "telegram adapter: reply failed"));
|
|
1374
|
-
}
|
|
1375
|
-
async function sendOnboardingGuide(chatId, threadId) {
|
|
1376
|
-
const bot = await resolveBotIdentity();
|
|
1377
|
-
if (stopped)
|
|
1378
|
-
return;
|
|
1379
|
-
await client.sendMessage(chatId, onboardingGuide(bot?.username), threadOpts(threadId)).catch((err) => logger.warn({ err, chatId }, "telegram adapter: onboarding guide failed"));
|
|
1380
|
-
}
|
|
1381
|
-
function materializeVisibleTopics() {
|
|
1382
|
-
if (!forumMode || !forumManageTopicsAvailable)
|
|
1383
|
-
return;
|
|
1384
|
-
for (const topic of listTopics()) {
|
|
1385
|
-
if (isTopicVisible(topic) && topic.participants.some((participant) => participant.userId === userId)) {
|
|
1386
|
-
materializeTopic(topic);
|
|
1387
|
-
}
|
|
1388
|
-
}
|
|
1389
|
-
}
|
|
1390
|
-
function restoreForumTopicCreation({ retryPermanent = false } = {}) {
|
|
1391
|
-
forumManageTopicsAvailable = true;
|
|
1392
|
-
permissionBlockedTopics.clear();
|
|
1393
|
-
if (retryPermanent) {
|
|
1394
|
-
materializeTombstones.clear();
|
|
1395
|
-
store.clearTombstones();
|
|
1396
|
-
}
|
|
1397
|
-
reconcileStaleMappings();
|
|
1398
|
-
materializeVisibleTopics();
|
|
1399
|
-
}
|
|
1400
|
-
async function notifyOwnerDms(text) {
|
|
1401
|
-
await Promise.allSettled([...ownerDmChatIds].map((chatId) => client.sendMessage(chatId, text)));
|
|
1402
|
-
}
|
|
1403
|
-
async function disconnectForum(chatId) {
|
|
1404
|
-
if (!forumMode || forumChat !== chatId)
|
|
1405
|
-
return false;
|
|
1406
|
-
for (const [topicId, pending] of pendingByTopic) {
|
|
1407
|
-
pending.cancelled = true;
|
|
1408
|
-
if (pendingByTopic.get(topicId) === pending)
|
|
1409
|
-
pendingByTopic.delete(topicId);
|
|
1410
|
-
}
|
|
1411
|
-
for (const mapping of [...byKey.values()]) {
|
|
1412
|
-
if (mapping.chatId === chatId)
|
|
1413
|
-
unloadMapping(mapping.chatId, mapping.threadId);
|
|
1414
|
-
}
|
|
1415
|
-
for (const [queryId, target] of targetByQueryId) {
|
|
1416
|
-
if (target.chatId === chatId)
|
|
1417
|
-
targetByQueryId.delete(queryId);
|
|
1418
|
-
}
|
|
1419
|
-
permissionBlockedTopics.clear();
|
|
1420
|
-
materializeTombstones.clear();
|
|
1421
|
-
store.clearTombstones();
|
|
1422
|
-
store.outboxDeleteByChat(chatId);
|
|
1423
|
-
store.clearForumChatId();
|
|
1424
|
-
forumMode = false;
|
|
1425
|
-
forumManageTopicsAvailable = false;
|
|
1426
|
-
forumChat = 0;
|
|
1427
|
-
await notifyOwnerDms("The Telegram forum was disconnected because the bot left or was removed. Your Negotium topics were preserved; promote the bot in a forum group to reconnect.");
|
|
1428
|
-
logger.info({ userId, forumChatId: chatId }, "telegram adapter: forum disconnected");
|
|
1429
|
-
return true;
|
|
1430
|
-
}
|
|
1431
|
-
async function linkForumAndAnnounce(ownerTelegramId, chat, botMember) {
|
|
1432
|
-
if (typeof client.createForumTopic !== "function") {
|
|
1433
|
-
reply(ownerTelegramId, undefined, "This Telegram client cannot create forum topics, so the group was not connected.");
|
|
1434
|
-
return false;
|
|
1435
|
-
}
|
|
1436
|
-
if (forumMode && forumChat !== chat.id) {
|
|
1437
|
-
reply(ownerTelegramId, undefined, `A different forum group (${forumChat}) is already connected to this Negotium node.`);
|
|
1438
|
-
return false;
|
|
1439
|
-
}
|
|
1440
|
-
const hasManageTopics = canManageTopics(botMember);
|
|
1441
|
-
if (forumMode && forumChat === chat.id) {
|
|
1442
|
-
const recovered = hasManageTopics && !forumManageTopicsAvailable;
|
|
1443
|
-
const lostPermission = !hasManageTopics && forumManageTopicsAvailable;
|
|
1444
|
-
if (hasManageTopics) {
|
|
1445
|
-
restoreForumTopicCreation({ retryPermanent: recovered });
|
|
1446
|
-
if (recovered) {
|
|
1447
|
-
await Promise.allSettled([
|
|
1448
|
-
client.sendMessage(chat.id, "Manage Topics permission confirmed. Pending Negotium topics are being created now."),
|
|
1449
|
-
notifyOwnerDms(`Manage Topics permission confirmed for \u201C${chat.title?.trim() || chat.id}\u201D. Pending topics are being retried.`)
|
|
1450
|
-
]);
|
|
1451
|
-
}
|
|
1452
|
-
} else {
|
|
1453
|
-
forumManageTopicsAvailable = false;
|
|
1454
|
-
if (lostPermission) {
|
|
1455
|
-
await notifyOwnerDms(`Manage Topics permission was removed from \u201C${chat.title?.trim() || chat.id}\u201D. Existing topics are preserved; restore the permission to resume topic creation.`);
|
|
1456
|
-
}
|
|
1457
|
-
}
|
|
1458
|
-
return true;
|
|
1459
|
-
}
|
|
1460
|
-
forumChat = chat.id;
|
|
1461
|
-
forumMode = true;
|
|
1462
|
-
forumManageTopicsAvailable = hasManageTopics;
|
|
1463
|
-
permissionBlockedTopics.clear();
|
|
1464
|
-
materializeTombstones.clear();
|
|
1465
|
-
store.clearTombstones();
|
|
1466
|
-
store.saveForumChatId(chat.id);
|
|
1467
|
-
bindMapping(chat.id, undefined, personalGeneral.id);
|
|
1468
|
-
ownerDmChatIds.add(ownerTelegramId);
|
|
1469
|
-
materializeVisibleTopics();
|
|
1470
|
-
const title = chat.title?.trim() || String(chat.id);
|
|
1471
|
-
const permissionWarning = hasManageTopics ? "" : `
|
|
1472
|
-
|
|
1473
|
-
Warning: enable the bot administrator permission "Manage Topics" before creating or deleting topics.`;
|
|
1474
|
-
await Promise.allSettled([
|
|
1475
|
-
client.sendMessage(chat.id, `Negotium connected to \u201C${title}\u201D. Use this General topic to create and manage topics in natural language.${permissionWarning}`),
|
|
1476
|
-
client.sendMessage(ownerTelegramId, `Connected forum group \u201C${title}\u201D (${chat.id}). No /connect command is needed.${permissionWarning}`)
|
|
1477
|
-
]);
|
|
1478
|
-
logger.info({
|
|
1479
|
-
userId,
|
|
1480
|
-
ownerTelegramId,
|
|
1481
|
-
forumChatId: chat.id,
|
|
1482
|
-
canManageTopics: hasManageTopics
|
|
1483
|
-
}, "telegram adapter: auto-connected forum group");
|
|
1484
|
-
return true;
|
|
1485
|
-
}
|
|
1486
|
-
async function tryAutoConnectFromMessage(msg) {
|
|
1487
|
-
const senderId = msg.from?.id;
|
|
1488
|
-
if (msg.chat.type !== "supergroup" || !msg.chat.is_forum || senderId === undefined || !isAllowed(senderId)) {
|
|
1489
|
-
return false;
|
|
1490
|
-
}
|
|
1491
|
-
if (forumMode && msg.chat.id !== forumChat)
|
|
1492
|
-
return false;
|
|
1493
|
-
if (forumMode && forumManageTopicsAvailable)
|
|
1494
|
-
return true;
|
|
1495
|
-
if (typeof client.getMe !== "function" || typeof client.getChatMember !== "function") {
|
|
1496
|
-
return false;
|
|
1497
|
-
}
|
|
1498
|
-
try {
|
|
1499
|
-
const bot = await resolveBotIdentity();
|
|
1500
|
-
if (!bot)
|
|
1501
|
-
return false;
|
|
1502
|
-
const [botMember, senderMember] = await Promise.all([
|
|
1503
|
-
client.getChatMember(msg.chat.id, bot.id),
|
|
1504
|
-
client.getChatMember(msg.chat.id, senderId)
|
|
1505
|
-
]);
|
|
1506
|
-
if (!isChatAdmin(botMember) || !isChatAdmin(senderMember))
|
|
1507
|
-
return false;
|
|
1508
|
-
await linkForumAndAnnounce(senderId, msg.chat, botMember);
|
|
1509
|
-
return forumMode && forumChat === msg.chat.id;
|
|
1510
|
-
} catch (err) {
|
|
1511
|
-
logger.debug({ err, groupId: msg.chat.id, senderId }, "telegram adapter: lazy forum auto-connect check failed");
|
|
1512
|
-
return false;
|
|
1513
|
-
}
|
|
1514
|
-
}
|
|
1515
|
-
async function verifyInitialForumPermissions() {
|
|
1516
|
-
if (!forumMode)
|
|
1517
|
-
return;
|
|
1518
|
-
if (forumChatId !== undefined)
|
|
1519
|
-
return;
|
|
1520
|
-
if (typeof client.getMe !== "function" || typeof client.getChatMember !== "function") {
|
|
1521
|
-
forumManageTopicsAvailable = true;
|
|
1522
|
-
return;
|
|
1523
|
-
}
|
|
1524
|
-
try {
|
|
1525
|
-
const bot = await resolveBotIdentity();
|
|
1526
|
-
if (!bot || stopped || !forumMode)
|
|
1527
|
-
return;
|
|
1528
|
-
const member = await client.getChatMember(forumChat, bot.id);
|
|
1529
|
-
if (member.status === "left" || member.status === "kicked") {
|
|
1530
|
-
await disconnectForum(forumChat);
|
|
1531
|
-
return;
|
|
1532
|
-
}
|
|
1533
|
-
if (canManageTopics(member)) {
|
|
1534
|
-
const restoredAutoConnection = forumChatId === undefined && restoredForumChatId !== undefined;
|
|
1535
|
-
restoreForumTopicCreation({ retryPermanent: restoredAutoConnection });
|
|
1536
|
-
} else {
|
|
1537
|
-
forumManageTopicsAvailable = false;
|
|
1538
|
-
}
|
|
1539
|
-
} catch (err) {
|
|
1540
|
-
logger.warn({ err, forumChatId: forumChat }, "telegram adapter: initial forum permission check failed");
|
|
1541
|
-
}
|
|
1542
|
-
}
|
|
1543
|
-
verifyInitialForumPermissions();
|
|
1544
|
-
function runTurn(topic, prompt, chatId, threadId) {
|
|
1545
|
-
const target = {
|
|
1546
|
-
topicId: topic.id,
|
|
1547
|
-
chatId,
|
|
1548
|
-
...threadId !== undefined ? { threadId } : {}
|
|
1549
|
-
};
|
|
1550
|
-
const rememberTarget = (queryId2) => {
|
|
1551
|
-
targetByQueryId.set(queryId2, target);
|
|
1552
|
-
};
|
|
1553
|
-
const { queryId } = submitUserMessage({
|
|
1554
|
-
topic,
|
|
1555
|
-
userId,
|
|
1556
|
-
text: prompt,
|
|
1557
|
-
sourceAdapter: "telegram",
|
|
1558
|
-
visualTools: false,
|
|
1559
|
-
onDispatched: rememberTarget,
|
|
1560
|
-
startTurn: dispatchTurn
|
|
1561
|
-
});
|
|
1562
|
-
if (queryId)
|
|
1563
|
-
rememberTarget(queryId);
|
|
1564
|
-
}
|
|
1565
|
-
const inboundQueues = new Map;
|
|
1566
|
-
function enqueueInbound(chatId, threadId, task) {
|
|
1567
|
-
const key = mappingKey(chatId, threadId);
|
|
1568
|
-
const previous = inboundQueues.get(key);
|
|
1569
|
-
const run = async () => {
|
|
1570
|
-
if (stopped)
|
|
1571
|
-
return;
|
|
1572
|
-
await task();
|
|
1573
|
-
};
|
|
1574
|
-
const next = (previous ? previous.catch(() => {}).then(run) : run()).catch((err) => logger.warn({ err, chatId, threadId }, "telegram adapter: inbound task failed"));
|
|
1575
|
-
inboundQueues.set(key, next);
|
|
1576
|
-
next.then(() => {
|
|
1577
|
-
if (inboundQueues.get(key) === next)
|
|
1578
|
-
inboundQueues.delete(key);
|
|
1579
|
-
});
|
|
1580
|
-
}
|
|
1581
|
-
const transcriber = opts.transcribe ?? ((filePath) => transcribeAudio(filePath));
|
|
1582
|
-
const transcriptionAvailable = () => opts.transcribe !== undefined || isTranscriptionConfigured();
|
|
1583
|
-
async function downloadToTopic(topicId, fileId, filename) {
|
|
1584
|
-
const url = await client.getFileLink(fileId);
|
|
1585
|
-
const res = await fetch(url);
|
|
1586
|
-
if (!res.ok)
|
|
1587
|
-
throw new Error(`attachment download failed: HTTP ${res.status}`);
|
|
1588
|
-
return ingestAttachment({
|
|
1589
|
-
topicId,
|
|
1590
|
-
filename,
|
|
1591
|
-
bytes: new Uint8Array(await res.arrayBuffer())
|
|
1592
|
-
});
|
|
1593
|
-
}
|
|
1594
|
-
async function ingestMessageFiles(topicId, msg) {
|
|
1595
|
-
const lines = [];
|
|
1596
|
-
if (msg.photo && msg.photo.length > 0) {
|
|
1597
|
-
const photo = msg.photo[msg.photo.length - 1];
|
|
1598
|
-
lines.push((await downloadToTopic(topicId, photo.file_id, "photo.jpg")).promptLine);
|
|
1599
|
-
}
|
|
1600
|
-
if (msg.document) {
|
|
1601
|
-
const filename = msg.document.file_name || "file";
|
|
1602
|
-
lines.push((await downloadToTopic(topicId, msg.document.file_id, filename)).promptLine);
|
|
1603
|
-
}
|
|
1604
|
-
return lines;
|
|
1605
|
-
}
|
|
1606
|
-
async function handleMediaMessage(msg, chatId, threadId) {
|
|
1607
|
-
const caption = (msg.text ?? msg.caption ?? "").trim();
|
|
1608
|
-
const topic = resolveMapping(chatId, threadId);
|
|
1609
|
-
if (typeof client.getFileLink !== "function") {
|
|
1610
|
-
logger.warn({ chatId }, "telegram adapter: media message but client lacks getFileLink \u2014 caption only");
|
|
1611
|
-
if (caption)
|
|
1612
|
-
runTurn(topic, caption, chatId, threadId);
|
|
1613
|
-
else
|
|
1614
|
-
reply(chatId, threadId, "this bot cannot download attachments");
|
|
1615
|
-
return;
|
|
1616
|
-
}
|
|
1617
|
-
let promptLines = [];
|
|
1618
|
-
let voiceText = "";
|
|
1619
|
-
try {
|
|
1620
|
-
promptLines = await ingestMessageFiles(topic.id, msg);
|
|
1621
|
-
if (msg.voice) {
|
|
1622
|
-
if (!transcriptionAvailable()) {
|
|
1623
|
-
reply(chatId, threadId, "voice transcription is not configured on this bot \u2014 please send text");
|
|
1624
|
-
} else {
|
|
1625
|
-
const ingested = await downloadToTopic(topic.id, msg.voice.file_id, "voice.ogg");
|
|
1626
|
-
const transcript = (await transcriber(ingested.path))?.trim();
|
|
1627
|
-
if (transcript) {
|
|
1628
|
-
voiceText = `[Voice transcript]
|
|
1629
|
-
${transcript}`;
|
|
1630
|
-
} else {
|
|
1631
|
-
reply(chatId, threadId, "voice transcription failed \u2014 please send text");
|
|
1632
|
-
}
|
|
1633
|
-
}
|
|
1634
|
-
}
|
|
1635
|
-
} catch (err) {
|
|
1636
|
-
logger.warn({ err, chatId, topicId: topic.id }, "telegram adapter: attachment intake failed");
|
|
1637
|
-
reply(chatId, threadId, errMsg(err, "attachment download failed"));
|
|
1638
|
-
}
|
|
1639
|
-
if (stopped)
|
|
1640
|
-
return;
|
|
1641
|
-
const userText = voiceText ? caption ? `${voiceText}
|
|
1642
|
-
|
|
1643
|
-
${caption}` : voiceText : caption;
|
|
1644
|
-
if (!userText && promptLines.length === 0)
|
|
1645
|
-
return;
|
|
1646
|
-
runTurn(topic, composeAttachmentPrompt(userText, promptLines), chatId, threadId);
|
|
1647
|
-
}
|
|
1648
|
-
const mediaGroupCfg = {
|
|
1649
|
-
debounceMs: opts.mediaGroup?.debounceMs ?? 1000,
|
|
1650
|
-
maxWaitMs: opts.mediaGroup?.maxWaitMs ?? 3000
|
|
1651
|
-
};
|
|
1652
|
-
const mediaGroups = new Map;
|
|
1653
|
-
function flushMediaGroup(key) {
|
|
1654
|
-
const entry = mediaGroups.get(key);
|
|
1655
|
-
if (!entry)
|
|
1656
|
-
return;
|
|
1657
|
-
mediaGroups.delete(key);
|
|
1658
|
-
entry.release();
|
|
1659
|
-
}
|
|
1660
|
-
function bufferMediaGroup(msg, chatId, threadId) {
|
|
1661
|
-
const key = `${chatId}:${msg.media_group_id}`;
|
|
1662
|
-
const existing = mediaGroups.get(key);
|
|
1663
|
-
if (existing) {
|
|
1664
|
-
existing.messages.push(msg);
|
|
1665
|
-
clearTimeout(existing.timer);
|
|
1666
|
-
const remaining = existing.firstSeenAt + mediaGroupCfg.maxWaitMs - Date.now();
|
|
1667
|
-
const wait = Math.max(0, Math.min(mediaGroupCfg.debounceMs, remaining));
|
|
1668
|
-
existing.timer = setTimeout(() => flushMediaGroup(key), wait);
|
|
1669
|
-
return;
|
|
1670
|
-
}
|
|
1671
|
-
let releaseReady;
|
|
1672
|
-
const ready = new Promise((resolve) => {
|
|
1673
|
-
releaseReady = resolve;
|
|
1674
|
-
});
|
|
1675
|
-
const entry = {
|
|
1676
|
-
messages: [msg],
|
|
1677
|
-
chatId,
|
|
1678
|
-
...threadId !== undefined ? { threadId } : {},
|
|
1679
|
-
firstSeenAt: Date.now(),
|
|
1680
|
-
timer: setTimeout(() => flushMediaGroup(key), Math.min(mediaGroupCfg.debounceMs, mediaGroupCfg.maxWaitMs)),
|
|
1681
|
-
ready,
|
|
1682
|
-
release: releaseReady
|
|
1683
|
-
};
|
|
1684
|
-
mediaGroups.set(key, entry);
|
|
1685
|
-
enqueueInbound(chatId, threadId, async () => {
|
|
1686
|
-
await entry.ready;
|
|
1687
|
-
if (stopped)
|
|
1688
|
-
return;
|
|
1689
|
-
await handleMediaGroupFlush(entry);
|
|
1690
|
-
});
|
|
1691
|
-
}
|
|
1692
|
-
async function handleMediaGroupFlush(entry) {
|
|
1693
|
-
const { messages, chatId, threadId } = entry;
|
|
1694
|
-
const topic = resolveMapping(chatId, threadId);
|
|
1695
|
-
const captions = messages.map((m) => (m.text ?? m.caption ?? "").trim()).filter((caption) => caption.length > 0);
|
|
1696
|
-
if (typeof client.getFileLink !== "function") {
|
|
1697
|
-
logger.warn({ chatId }, "telegram adapter: media group but client lacks getFileLink \u2014 captions only");
|
|
1698
|
-
if (captions.length > 0)
|
|
1699
|
-
runTurn(topic, captions.join(`
|
|
1700
|
-
`), chatId, threadId);
|
|
1701
|
-
else
|
|
1702
|
-
reply(chatId, threadId, "this bot cannot download attachments");
|
|
1703
|
-
return;
|
|
1704
|
-
}
|
|
1705
|
-
const promptLines = [];
|
|
1706
|
-
for (const msg of messages) {
|
|
1707
|
-
try {
|
|
1708
|
-
promptLines.push(...await ingestMessageFiles(topic.id, msg));
|
|
1709
|
-
} catch (err) {
|
|
1710
|
-
logger.warn({ err, chatId, topicId: topic.id }, "telegram adapter: media group attachment intake failed");
|
|
1711
|
-
reply(chatId, threadId, errMsg(err, "attachment download failed"));
|
|
1712
|
-
}
|
|
1713
|
-
}
|
|
1714
|
-
if (stopped)
|
|
1715
|
-
return;
|
|
1716
|
-
const userText = captions.join(`
|
|
1717
|
-
`);
|
|
1718
|
-
if (!userText && promptLines.length === 0)
|
|
1719
|
-
return;
|
|
1720
|
-
runTurn(topic, composeAttachmentPrompt(userText, promptLines), chatId, threadId);
|
|
1721
|
-
}
|
|
1722
|
-
async function handleCommand(text, chatId, threadId, telegramUserId) {
|
|
1723
|
-
const [rawCmd = ""] = text.split(/\s+/);
|
|
1724
|
-
const addressedUsername = rawCmd.match(/@(\w+)$/)?.[1];
|
|
1725
|
-
if (addressedUsername) {
|
|
1726
|
-
const bot = await resolveBotIdentity();
|
|
1727
|
-
if (!bot?.username || bot.username.toLowerCase() !== addressedUsername.toLowerCase())
|
|
1728
|
-
return;
|
|
1729
|
-
}
|
|
1730
|
-
if (isVaultCommandLine(text)) {
|
|
1731
|
-
if (!isVaultOwner(telegramUserId))
|
|
1732
|
-
return;
|
|
1733
|
-
const vaultResponse = executeVaultCommand(userId, text);
|
|
1734
|
-
if (vaultResponse !== null)
|
|
1735
|
-
reply(chatId, threadId, vaultResponse);
|
|
1736
|
-
return;
|
|
1737
|
-
}
|
|
1738
|
-
const cmd = rawCmd.replace(/@\w+$/, "");
|
|
1739
|
-
const arg = extractCommandArg(text);
|
|
1740
|
-
switch (cmd) {
|
|
1741
|
-
case "/start": {
|
|
1742
|
-
await sendOnboardingGuide(chatId, threadId);
|
|
1743
|
-
return;
|
|
1744
|
-
}
|
|
1745
|
-
case "/abort": {
|
|
1746
|
-
const mapping = byKey.get(mappingKey(chatId, threadId));
|
|
1747
|
-
reply(chatId, threadId, mapping && topicService.abortTurn(mapping.topicId, userId) ? "aborted" : "nothing running");
|
|
1748
|
-
return;
|
|
1749
|
-
}
|
|
1750
|
-
case "/agent": {
|
|
1751
|
-
if (!isAgentKind(arg)) {
|
|
1752
|
-
reply(chatId, threadId, "usage: /agent <claude|codex|maestro>");
|
|
1753
|
-
return;
|
|
1754
|
-
}
|
|
1755
|
-
try {
|
|
1756
|
-
const topic = getOrCreateTopic(`${titleFor(chatId, threadId)}-${arg}`, arg);
|
|
1757
|
-
bindMapping(chatId, threadId, topic.id);
|
|
1758
|
-
reply(chatId, threadId, `agent set to ${arg} \u2014 topic "${topic.title}"`);
|
|
1759
|
-
} catch (err) {
|
|
1760
|
-
reply(chatId, threadId, errMsg(err, "agent switch failed"));
|
|
1761
|
-
}
|
|
1762
|
-
return;
|
|
1763
|
-
}
|
|
1764
|
-
case "/topics": {
|
|
1765
|
-
const mine = listTopics().filter((t) => isTopicVisible(t) && t.participants.some((p) => p.userId === userId));
|
|
1766
|
-
reply(chatId, threadId, mine.length ? mine.map((t) => `- ${t.title}${t.agent ? ` (${t.agent})` : ""}`).join(`
|
|
1767
|
-
`) : "no topics");
|
|
1768
|
-
return;
|
|
1769
|
-
}
|
|
1770
|
-
case "/new": {
|
|
1771
|
-
if (!arg) {
|
|
1772
|
-
const mapping = byKey.get(mappingKey(chatId, threadId));
|
|
1773
|
-
const topic = mapping ? getTopic(mapping.topicId) : null;
|
|
1774
|
-
if (!topic) {
|
|
1775
|
-
reply(chatId, threadId, "nothing to reset \u2014 this chat has no topic yet");
|
|
1776
|
-
return;
|
|
1777
|
-
}
|
|
1778
|
-
try {
|
|
1779
|
-
const result = await topicService.reset({
|
|
1780
|
-
topicId: topic.id,
|
|
1781
|
-
userId,
|
|
1782
|
-
reason: "telegram-session-reset"
|
|
1783
|
-
});
|
|
1784
|
-
reply(chatId, threadId, result.text);
|
|
1785
|
-
} catch (err) {
|
|
1786
|
-
reply(chatId, threadId, errMsg(err, "session reset failed"));
|
|
1787
|
-
}
|
|
1788
|
-
return;
|
|
1789
|
-
}
|
|
1790
|
-
try {
|
|
1791
|
-
const createOptions = {
|
|
1792
|
-
title: arg,
|
|
1793
|
-
userId,
|
|
1794
|
-
kind: "agent",
|
|
1795
|
-
...opts.defaultAgent ? { agent: opts.defaultAgent } : {}
|
|
1796
|
-
};
|
|
1797
|
-
const fromForumGeneral = forumMode && chatId === forumChat && threadId === undefined;
|
|
1798
|
-
const topic = fromForumGeneral ? topicService.create(createOptions) : registerTopicLocal(createOptions);
|
|
1799
|
-
if (fromForumGeneral) {
|
|
1800
|
-
reply(chatId, threadId, `creating new topic "${topic.title}"`);
|
|
1801
|
-
return;
|
|
1802
|
-
}
|
|
1803
|
-
bindMapping(chatId, threadId, topic.id);
|
|
1804
|
-
reply(chatId, threadId, `switched to new topic "${topic.title}"`);
|
|
1805
|
-
} catch (err) {
|
|
1806
|
-
if (err instanceof TopicValidationError) {
|
|
1807
|
-
reply(chatId, threadId, err.message);
|
|
1808
|
-
} else {
|
|
1809
|
-
reply(chatId, threadId, errMsg(err, "topic create failed"));
|
|
1810
|
-
}
|
|
1811
|
-
}
|
|
1812
|
-
return;
|
|
1813
|
-
}
|
|
1814
|
-
case "/load": {
|
|
1815
|
-
if (!arg) {
|
|
1816
|
-
reply(chatId, threadId, "usage: /load <topic>");
|
|
1817
|
-
return;
|
|
1818
|
-
}
|
|
1819
|
-
const candidate = getTopicByNameForUser(arg, userId) ?? getTopic(arg);
|
|
1820
|
-
const topic = candidate && isTopicVisible(candidate) ? candidate : null;
|
|
1821
|
-
if (!topic || !loadExistingTopic(chatId, topic.id, threadId)) {
|
|
1822
|
-
reply(chatId, threadId, `no visible topic matching "${arg}"`);
|
|
1823
|
-
return;
|
|
1824
|
-
}
|
|
1825
|
-
reply(chatId, threadId, `loaded topic "${topic.title}"`);
|
|
1826
|
-
return;
|
|
1827
|
-
}
|
|
1828
|
-
case "/unload": {
|
|
1829
|
-
if (!unloadMapping(chatId, threadId)) {
|
|
1830
|
-
reply(chatId, threadId, "this chat has no loaded topic");
|
|
1831
|
-
return;
|
|
1832
|
-
}
|
|
1833
|
-
reply(chatId, threadId, "unloaded topic; the Negotium topic was preserved");
|
|
1834
|
-
return;
|
|
1835
|
-
}
|
|
1836
|
-
case "/fork":
|
|
1837
|
-
case "/spawn": {
|
|
1838
|
-
const label = cmd.slice(1);
|
|
1839
|
-
const mapping = byKey.get(mappingKey(chatId, threadId));
|
|
1840
|
-
if (!mapping) {
|
|
1841
|
-
reply(chatId, threadId, `nothing to ${label} \u2014 this chat has no topic yet`);
|
|
1842
|
-
return;
|
|
1843
|
-
}
|
|
1844
|
-
const copyHistory = cmd === "/fork";
|
|
1845
|
-
topicService.derive({
|
|
1846
|
-
sourceTopicId: mapping.topicId,
|
|
1847
|
-
userId,
|
|
1848
|
-
copyHistory,
|
|
1849
|
-
...arg ? { name: arg } : {}
|
|
1850
|
-
}).then((derived) => {
|
|
1851
|
-
if (!derived) {
|
|
1852
|
-
reply(chatId, threadId, `${label} failed`);
|
|
1853
|
-
return;
|
|
1854
|
-
}
|
|
1855
|
-
reply(chatId, threadId, `${copyHistory ? "forked into" : "spawned"} "${derived.title}"`);
|
|
1856
|
-
}).catch((err) => {
|
|
1857
|
-
reply(chatId, threadId, err instanceof TopicTitleConflictError ? err.message : errMsg(err, `${label} failed`));
|
|
1858
|
-
});
|
|
1859
|
-
return;
|
|
1860
|
-
}
|
|
1861
|
-
case "/del":
|
|
1862
|
-
case "/del!": {
|
|
1863
|
-
const force = cmd === "/del!";
|
|
1864
|
-
const mapping = byKey.get(mappingKey(chatId, threadId));
|
|
1865
|
-
const topic = arg ? getTopicByNameForUser(arg, userId) : mapping ? getTopic(mapping.topicId) : null;
|
|
1866
|
-
if (!topic) {
|
|
1867
|
-
reply(chatId, threadId, arg ? `no topic named "${arg}"` : "this chat has no topic \u2014 usage: /del [name]");
|
|
1868
|
-
return;
|
|
1869
|
-
}
|
|
1870
|
-
reply(chatId, threadId, `deleting topic "${topic.title}"\u2026`);
|
|
1871
|
-
topicService.delete({ topicId: topic.id, userId, force }).catch((err) => {
|
|
1872
|
-
if (err instanceof TopicArchiveRequiredError) {
|
|
1873
|
-
reply(chatId, threadId, `delete blocked: archiving "${topic.title}" failed and deleting now would lose its history. ` + `Retry after fixing the archive, or force with: /del!${arg ? ` ${arg}` : ""}`);
|
|
1874
|
-
} else {
|
|
1875
|
-
reply(chatId, threadId, errMsg(err, "delete failed"));
|
|
1876
|
-
}
|
|
1877
|
-
});
|
|
1878
|
-
return;
|
|
1879
|
-
}
|
|
1880
|
-
default:
|
|
1881
|
-
reply(chatId, threadId, "commands: /new [name], /topics, /agent <claude|codex|maestro>, " + "/load <topic>, /unload, /fork [name], /spawn [name], " + "/del [name], /del! [name], /abort, /vault <list|set|del>");
|
|
1882
|
-
}
|
|
1883
|
-
}
|
|
1884
|
-
function handleIncomingMessage(msg) {
|
|
1885
|
-
if (stopped)
|
|
1886
|
-
return;
|
|
1887
|
-
const chatId = msg.chat.id;
|
|
1888
|
-
const generalForumMessage = msg.chat.type === "supergroup" && msg.chat.is_forum === true && msg.is_topic_message !== true;
|
|
1889
|
-
const threadId = generalForumMessage ? undefined : msg.message_thread_id;
|
|
1890
|
-
const text = msg.text?.trim();
|
|
1891
|
-
const hasMedia = Boolean(msg.photo?.length || msg.document || msg.voice);
|
|
1892
|
-
const privateDm = msg.chat.type === "private";
|
|
1893
|
-
const firstPrivateContact = privateDm && !byKey.has(mappingKey(chatId));
|
|
1894
|
-
if (privateDm)
|
|
1895
|
-
ownerDmChatIds.add(chatId);
|
|
1896
|
-
if (privateDm || forumMode && chatId === forumChat && threadId === undefined) {
|
|
1897
|
-
bindMapping(chatId, threadId, personalGeneral.id);
|
|
1898
|
-
}
|
|
1899
|
-
if (firstPrivateContact && text !== "/start") {
|
|
1900
|
-
sendOnboardingGuide(chatId);
|
|
1901
|
-
}
|
|
1902
|
-
if (text?.startsWith("/")) {
|
|
1903
|
-
const command = text.split(/\s+/, 1)[0]?.replace(/@\w+$/, "");
|
|
1904
|
-
if (command === "/abort") {
|
|
1905
|
-
handleCommand(text, chatId, threadId, msg.from?.id);
|
|
1906
|
-
} else {
|
|
1907
|
-
enqueueInbound(chatId, threadId, () => handleCommand(text, chatId, threadId, msg.from?.id));
|
|
1908
|
-
}
|
|
1909
|
-
return;
|
|
1910
|
-
}
|
|
1911
|
-
if (hasMedia) {
|
|
1912
|
-
if (msg.media_group_id) {
|
|
1913
|
-
bufferMediaGroup(msg, chatId, threadId);
|
|
1914
|
-
return;
|
|
1915
|
-
}
|
|
1916
|
-
enqueueInbound(chatId, threadId, () => handleMediaMessage(msg, chatId, threadId));
|
|
1917
|
-
return;
|
|
1918
|
-
}
|
|
1919
|
-
if (!text)
|
|
1920
|
-
return;
|
|
1921
|
-
enqueueInbound(chatId, threadId, () => runTurn(resolveMapping(chatId, threadId), text, chatId, threadId));
|
|
1922
|
-
}
|
|
1923
|
-
client.on("message", (msg) => {
|
|
1924
|
-
if (stopped)
|
|
1925
|
-
return;
|
|
1926
|
-
if (!isAllowed(msg.from?.id))
|
|
1927
|
-
return;
|
|
1928
|
-
if (msg.chat.type === "supergroup" && msg.chat.is_forum) {
|
|
1929
|
-
if (forumMode && msg.chat.id !== forumChat)
|
|
1930
|
-
return;
|
|
1931
|
-
if (!forumMode || !forumManageTopicsAvailable) {
|
|
1932
|
-
tryAutoConnectFromMessage(msg).then((connected) => {
|
|
1933
|
-
if (connected && !stopped)
|
|
1934
|
-
handleIncomingMessage(msg);
|
|
1935
|
-
});
|
|
1936
|
-
return;
|
|
1937
|
-
}
|
|
1938
|
-
}
|
|
1939
|
-
handleIncomingMessage(msg);
|
|
1940
|
-
});
|
|
1941
|
-
client.on("my_chat_member", (update) => {
|
|
1942
|
-
if (stopped || update.chat.type !== "supergroup" || !update.chat.is_forum)
|
|
1943
|
-
return;
|
|
1944
|
-
const status = update.new_chat_member?.status;
|
|
1945
|
-
if ((status === "left" || status === "kicked") && forumChat === update.chat.id) {
|
|
1946
|
-
disconnectForum(update.chat.id).catch((err) => logger.warn({ err, groupId: update.chat.id }, "telegram adapter: forum disconnect failed"));
|
|
1947
|
-
return;
|
|
1948
|
-
}
|
|
1949
|
-
if (forumMode && forumChat === update.chat.id && status !== "administrator" && status !== "creator") {
|
|
1950
|
-
const permissionWasAvailable = forumManageTopicsAvailable;
|
|
1951
|
-
forumManageTopicsAvailable = false;
|
|
1952
|
-
if (permissionWasAvailable) {
|
|
1953
|
-
notifyOwnerDms('The bot no longer has forum administrator access. Existing topics are preserved; restore administrator + "Manage Topics" to resume topic creation.');
|
|
1954
|
-
}
|
|
1955
|
-
return;
|
|
1956
|
-
}
|
|
1957
|
-
if (status !== "administrator" && status !== "creator" || update.from?.id === undefined || !isAllowed(update.from.id) && (!forumMode || forumChat !== update.chat.id)) {
|
|
1958
|
-
return;
|
|
1959
|
-
}
|
|
1960
|
-
linkForumAndAnnounce(update.from.id, update.chat, update.new_chat_member).catch((err) => logger.warn({ err, groupId: update.chat.id }, "telegram adapter: forum auto-connect failed"));
|
|
1961
|
-
});
|
|
1962
|
-
const unsubscribe = runtimeBus().subscribe((event) => {
|
|
1963
|
-
if (stopped)
|
|
1964
|
-
return;
|
|
1965
|
-
if (event.type === "topic-created" && forumMode) {
|
|
1966
|
-
materializeTopic(event.payload);
|
|
1967
|
-
return;
|
|
1968
|
-
}
|
|
1969
|
-
if (event.type === "topic-deleted") {
|
|
1970
|
-
handleTopicDeleted(event.topicId);
|
|
1971
|
-
return;
|
|
1972
|
-
}
|
|
1973
|
-
if (event.type === "message-updated") {
|
|
1974
|
-
const payload = event.payload;
|
|
1975
|
-
if (!payload.messageId || !payload.patch)
|
|
1976
|
-
return;
|
|
1977
|
-
if (payload.patch.deleted) {
|
|
1978
|
-
deleteDeliveredRuntimeMessage(event.topicId, payload.messageId);
|
|
1979
|
-
return;
|
|
1980
|
-
}
|
|
1981
|
-
const current = runtimeMessages.get(payload.messageId);
|
|
1982
|
-
if (!current)
|
|
1983
|
-
return;
|
|
1984
|
-
const updated = { ...current, ...payload.patch };
|
|
1985
|
-
runtimeMessages.set(payload.messageId, updated);
|
|
1986
|
-
if (footerEnabled && payload.patch.usage && updated.authorId === "ai") {
|
|
1987
|
-
const footer = renderTurnFooter(updated);
|
|
1988
|
-
if (footer)
|
|
1989
|
-
attachUpdatedFooter(event.topicId, payload.messageId, footer);
|
|
1990
|
-
}
|
|
1991
|
-
return;
|
|
1992
|
-
}
|
|
1993
|
-
if (event.type === "ai-status") {
|
|
1994
|
-
const status = event.payload;
|
|
1995
|
-
if (status?.kind === "ai_active" && typeof client.sendChatAction === "function") {
|
|
1996
|
-
if (status.queryId)
|
|
1997
|
-
startTypingHeartbeat(event.topicId, status.queryId);
|
|
1998
|
-
else
|
|
1999
|
-
sendTyping(event.topicId);
|
|
2000
|
-
}
|
|
2001
|
-
if (status?.queryId && (status.kind === "ai_done" || status.kind === "ai_error" || status.kind === "ai_aborted")) {
|
|
2002
|
-
closeToolStatus(status.queryId);
|
|
2003
|
-
stopTypingHeartbeat(status.queryId);
|
|
2004
|
-
targetByQueryId.delete(status.queryId);
|
|
2005
|
-
clearQueryDeliveryState(status.queryId);
|
|
2006
|
-
}
|
|
2007
|
-
if (status?.kind === "tool_call" && status.queryId && status.label) {
|
|
2008
|
-
showToolStatus(event.topicId, status.queryId, status.label);
|
|
2009
|
-
}
|
|
2010
|
-
return;
|
|
2011
|
-
}
|
|
2012
|
-
if (event.type !== "message")
|
|
2013
|
-
return;
|
|
2014
|
-
const msg = event.payload;
|
|
2015
|
-
if (msg.authorId === userId && msg.sourceAdapter === "telegram")
|
|
2016
|
-
return;
|
|
2017
|
-
if (msg.kind === "tool")
|
|
2018
|
-
return;
|
|
2019
|
-
if (!msg.text)
|
|
2020
|
-
return;
|
|
2021
|
-
const runtimeMessageId = msg.authorId === "ai" ? msg.id : undefined;
|
|
2022
|
-
if (runtimeMessageId) {
|
|
2023
|
-
runtimeMessages.set(runtimeMessageId, msg);
|
|
2024
|
-
if (!msg.queryId) {
|
|
2025
|
-
const timer = setTimeout(() => {
|
|
2026
|
-
runtimeMessages.delete(runtimeMessageId);
|
|
2027
|
-
deliveredByRuntimeMessageId.delete(runtimeMessageId);
|
|
2028
|
-
}, 5 * 60000);
|
|
2029
|
-
timer.unref?.();
|
|
2030
|
-
}
|
|
2031
|
-
}
|
|
2032
|
-
const files = extractFileTagPaths(msg.text);
|
|
2033
|
-
const rawText = files.length > 0 ? stripFileTags(msg.text) : msg.text;
|
|
2034
|
-
const text = msg.authorId === userId && rawText ? `[From: User] ${rawText}` : rawText;
|
|
2035
|
-
if (!text && files.length === 0)
|
|
2036
|
-
return;
|
|
2037
|
-
routeMessage(event.topicId, { text, files, runtimeMessageId }, msg.queryId);
|
|
2038
|
-
});
|
|
2039
|
-
if (forumMode) {
|
|
2040
|
-
bindMapping(forumChat, undefined, personalGeneral.id);
|
|
2041
|
-
materializeVisibleTopics();
|
|
2042
|
-
}
|
|
2043
|
-
return {
|
|
2044
|
-
name: "telegram",
|
|
2045
|
-
loadTopic: loadExistingTopic,
|
|
2046
|
-
unloadTopic: unloadMapping,
|
|
2047
|
-
stop() {
|
|
2048
|
-
if (stopped)
|
|
2049
|
-
return;
|
|
2050
|
-
stopped = true;
|
|
2051
|
-
outboxWorker.stop();
|
|
2052
|
-
for (const timer of typingHeartbeatByQueryId.values())
|
|
2053
|
-
clearInterval(timer);
|
|
2054
|
-
typingHeartbeatByQueryId.clear();
|
|
2055
|
-
for (const queryId of toolStatusByQueryId.keys())
|
|
2056
|
-
closeToolStatus(queryId);
|
|
2057
|
-
for (const entry of mediaGroups.values()) {
|
|
2058
|
-
clearTimeout(entry.timer);
|
|
2059
|
-
entry.release();
|
|
2060
|
-
}
|
|
2061
|
-
mediaGroups.clear();
|
|
2062
|
-
inboundQueues.clear();
|
|
2063
|
-
unsubscribe();
|
|
2064
|
-
store.close();
|
|
2065
|
-
}
|
|
2066
|
-
};
|
|
2067
|
-
}
|
|
2068
|
-
// ../../adapters/telegram/src/index.ts
|
|
2069
|
-
var telegramAdapter = defineNegotiumAdapter({
|
|
2070
|
-
name: "telegram",
|
|
2071
|
-
capabilities: {
|
|
2072
|
-
localUserInput: true,
|
|
2073
|
-
topicManagement: true,
|
|
2074
|
-
externalPlacedTurn: false
|
|
2075
|
-
},
|
|
2076
|
-
projection: {
|
|
2077
|
-
transcript: "live-only",
|
|
2078
|
-
historyBackfill: false,
|
|
2079
|
-
externalAuthors: "relayed"
|
|
2080
|
-
},
|
|
2081
|
-
start: startTelegramAdapter
|
|
2082
|
-
});
|
|
2083
|
-
|
|
2084
|
-
// ../../adapters/telegram/src/cli.ts
|
|
2085
|
-
function startTelegramFromEnv() {
|
|
2086
|
-
const token = process.env.TELEGRAM_BOT_TOKEN?.trim();
|
|
2087
|
-
if (!token)
|
|
2088
|
-
throw new Error("TELEGRAM_BOT_TOKEN is required");
|
|
2089
|
-
const bot = new TelegramBot(token, { polling: true });
|
|
2090
|
-
const requestedAgent = process.env.TELEGRAM_DEFAULT_AGENT?.trim();
|
|
2091
|
-
const forumChatId = Number.parseInt(process.env.TELEGRAM_FORUM_CHAT_ID ?? "", 10);
|
|
2092
|
-
const adapter = startTelegramAdapter({
|
|
2093
|
-
client: bot,
|
|
2094
|
-
allowedUsers: (process.env.TELEGRAM_ALLOWED_USERS ?? "").split(",").map((value) => value.trim()).filter(Boolean),
|
|
2095
|
-
...process.env.TELEGRAM_VAULT_OWNER_USER_ID?.trim() ? { vaultOwnerTelegramUserId: process.env.TELEGRAM_VAULT_OWNER_USER_ID.trim() } : {},
|
|
2096
|
-
...isAgentKind(requestedAgent) ? { defaultAgent: requestedAgent } : {},
|
|
2097
|
-
...Number.isFinite(forumChatId) ? { forumChatId } : {}
|
|
2098
|
-
});
|
|
2099
|
-
let stopped = false;
|
|
2100
|
-
return {
|
|
2101
|
-
name: "telegram",
|
|
2102
|
-
adapter,
|
|
2103
|
-
bot,
|
|
2104
|
-
async stop() {
|
|
2105
|
-
if (stopped)
|
|
2106
|
-
return;
|
|
2107
|
-
stopped = true;
|
|
2108
|
-
await adapter.stop();
|
|
2109
|
-
await bot.stopPolling({ cancel: true }).catch(() => {});
|
|
2110
|
-
}
|
|
2111
|
-
};
|
|
2112
|
-
}
|
|
2113
|
-
async function runTelegramCli() {
|
|
2114
|
-
if (!process.env.TELEGRAM_BOT_TOKEN?.trim()) {
|
|
2115
|
-
throw new Error("TELEGRAM_BOT_TOKEN is required");
|
|
2116
|
-
}
|
|
2117
|
-
let node;
|
|
2118
|
-
const singleton = await waitForRequiredRuntimeProcessLease("adapter:telegram", {
|
|
2119
|
-
workloadName: "Telegram adapter",
|
|
2120
|
-
onLost: () => {
|
|
2121
|
-
process.stderr.write(`negotium-telegram: singleton lease lost; shutting down
|
|
2122
|
-
`);
|
|
2123
|
-
node?.stop();
|
|
2124
|
-
}
|
|
2125
|
-
});
|
|
2126
|
-
try {
|
|
2127
|
-
node = await startDefaultNode({ port: 0 });
|
|
2128
|
-
} catch (error) {
|
|
2129
|
-
singleton.stop();
|
|
2130
|
-
throw error;
|
|
2131
|
-
}
|
|
2132
|
-
let channel;
|
|
2133
|
-
try {
|
|
2134
|
-
channel = startTelegramFromEnv();
|
|
2135
|
-
} catch (error) {
|
|
2136
|
-
await node.stop();
|
|
2137
|
-
singleton.stop();
|
|
2138
|
-
throw error;
|
|
2139
|
-
}
|
|
2140
|
-
onShutdown("telegram-channel", 100, () => channel.stop());
|
|
2141
|
-
onShutdown("telegram-singleton", 90, () => singleton.stop());
|
|
2142
|
-
process.stdout.write(`negotium Telegram adapter listening through node :${node.port}
|
|
2143
|
-
`);
|
|
2144
|
-
await node.completed;
|
|
2145
|
-
}
|
|
2146
|
-
if (false) {}
|
|
2147
|
-
export {
|
|
2148
|
-
startTelegramFromEnv,
|
|
2149
|
-
runTelegramCli
|
|
2150
|
-
};
|
|
2151
|
-
|
|
2152
|
-
//# debugId=42F29D9EFE4D3DE164756E2164756E21
|