@agentprojectcontext/apx 1.42.0 → 1.42.2
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/package.json +1 -1
- package/src/core/agent/constants.js +10 -0
- package/src/core/agent/run-agent.js +36 -18
- package/src/core/channels/telegram/api.js +62 -0
- package/src/core/channels/telegram/ask-callbacks.js +238 -0
- package/src/core/channels/telegram/dispatch.js +60 -310
- package/src/core/channels/telegram/helpers.js +28 -1
- package/src/core/channels/telegram/inbound/audio.js +82 -0
- package/src/core/channels/telegram/inbound/photo.js +63 -0
- package/src/core/channels/telegram/reply.js +204 -0
- package/src/core/config/index.js +5 -0
- package/src/core/confirmation/adapters/telegram.js +20 -37
- package/src/core/i18n/en.js +4 -0
- package/src/core/i18n/es.js +4 -0
- package/src/core/i18n/pt.js +4 -0
- package/src/host/daemon/plugins/desktop/index.js +6 -1
- package/src/host/daemon/plugins/telegram/index.js +62 -360
- package/src/interfaces/web/package-lock.json +3 -3
|
@@ -27,33 +27,16 @@
|
|
|
27
27
|
// "poll_interval_ms": 1500
|
|
28
28
|
// }
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
import {
|
|
37
|
-
import { compactChannelIfNeeded } from "#core/memory/index.js";
|
|
38
|
-
import { readAgents } from "#core/apc/parser.js";
|
|
39
|
-
import { buildAgentSystem } from "#core/agent/build-agent-system.js";
|
|
40
|
-
import { transcribe as transcribeAudioFile } from "#core/voice/transcription.js";
|
|
30
|
+
// This poller is intentionally thin: per-update logic lives in core/channels/
|
|
31
|
+
// telegram/ — dispatch (inbound routing), reply (the super-agent turn),
|
|
32
|
+
// ask-callbacks (the ask_questions flow), inbound/ (media), and the raw Bot API
|
|
33
|
+
// in api.js + media.js. The poller keeps only what the *running process* needs:
|
|
34
|
+
// lifecycle, the poll loop, offset state, and the thin I/O surface (self._send
|
|
35
|
+
// etc.) that the extracted core logic calls back into through `self`.
|
|
36
|
+
import { appendGlobalMessage } from "#core/stores/messages.js";
|
|
41
37
|
import { resolveAgentName, SUPERAGENT_ACTOR_ID } from "#core/identity/index.js";
|
|
42
|
-
import { registerSender, resolveAllowedTools } from "#core/identity/telegram.js";
|
|
43
|
-
import { buildRelationshipBlock } from "#core/agent/index.js";
|
|
44
|
-
import { getConfirmationStore as getConfirmStore } from "#core/confirmation/pending-store.js";
|
|
45
38
|
import { CHANNELS } from "#core/constants/channels.js";
|
|
46
|
-
import { tryResolveSkillCommand } from "#core/agent/skills/trigger.js";
|
|
47
|
-
import { createTelegramConfirmAdapter } from "#core/confirmation/adapters/telegram.js";
|
|
48
|
-
import * as askFlow from "#core/channels/telegram/ask.js";
|
|
49
|
-
|
|
50
|
-
// API_BASE re-imported from #core/channels/telegram/media.js below
|
|
51
|
-
const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
52
|
-
|
|
53
|
-
// All non-class-bound channel logic lives in core/channels/telegram/ — this
|
|
54
|
-
// file stays focused on the poller class + plugin lifecycle wiring.
|
|
55
39
|
import {
|
|
56
|
-
buildTelegramMeta,
|
|
57
40
|
loadState,
|
|
58
41
|
saveState,
|
|
59
42
|
resolveBotToken,
|
|
@@ -63,11 +46,13 @@ import {
|
|
|
63
46
|
sleep,
|
|
64
47
|
} from "#core/channels/telegram/helpers.js";
|
|
65
48
|
import { handleUpdate } from "#core/channels/telegram/dispatch.js";
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
import { sendPhoto, sendVoice, sendDocument, sendAudio
|
|
49
|
+
import { handleCallbackQuery, startAskFlow, maybeConsumeAskTextAnswer } from "#core/channels/telegram/ask-callbacks.js";
|
|
50
|
+
import { sendMessage, sendChatAction, editMessageReplyMarkup, answerCallbackQuery, getUpdates } from "#core/channels/telegram/api.js";
|
|
51
|
+
import { sendPhoto, sendVoice, sendDocument, sendAudio } from "#core/channels/telegram/media.js";
|
|
69
52
|
export { sendPhoto, sendVoice, sendDocument, sendAudio };
|
|
70
53
|
|
|
54
|
+
const nowIso = () => new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
55
|
+
|
|
71
56
|
// ---------- per-channel poller ----------------------------------------------
|
|
72
57
|
|
|
73
58
|
class ChannelPoller {
|
|
@@ -165,13 +150,7 @@ class ChannelPoller {
|
|
|
165
150
|
}
|
|
166
151
|
|
|
167
152
|
async _getUpdates() {
|
|
168
|
-
|
|
169
|
-
const url = `${API_BASE}/bot${token}/getUpdates?timeout=25&offset=${this.offset}`;
|
|
170
|
-
const res = await fetch(url);
|
|
171
|
-
if (!res.ok) throw new Error(`getUpdates ${res.status}`);
|
|
172
|
-
const json = await res.json();
|
|
173
|
-
if (!json.ok) throw new Error(json.description || "telegram error");
|
|
174
|
-
return json.result || [];
|
|
153
|
+
return getUpdates(resolveBotToken(this.channel), { offset: this.offset });
|
|
175
154
|
}
|
|
176
155
|
|
|
177
156
|
// Method body lives in ./dispatch.js as `handleUpdate(self, u)` so this file
|
|
@@ -181,270 +160,42 @@ class ChannelPoller {
|
|
|
181
160
|
return handleUpdate(this, u);
|
|
182
161
|
}
|
|
183
162
|
|
|
163
|
+
// ── ask_questions flow ──────────────────────────────────────────────────
|
|
164
|
+
// Orchestration lives in ./ask-callbacks.js (state machine in ./ask.js). These
|
|
165
|
+
// are thin delegates: dispatch.js reaches _startAskFlow / _maybeConsumeAsk...
|
|
166
|
+
// through `self`, and inbound callback_query routes through _handleCallbackQuery.
|
|
167
|
+
// The core functions call back into this poller's I/O surface (_send etc.).
|
|
184
168
|
async _handleCallbackQuery(callbackQuery) {
|
|
185
|
-
|
|
186
|
-
// both use `apx:<verb>:...` namespacing but ask owns its own state.
|
|
187
|
-
const data = callbackQuery.data || "";
|
|
188
|
-
if (data.startsWith("apx:ask:")) {
|
|
189
|
-
await this._handleAskCallback(callbackQuery);
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
const adapter = createTelegramConfirmAdapter({
|
|
194
|
-
token: resolveBotToken(this.channel),
|
|
195
|
-
chatId: callbackQuery.message?.chat?.id,
|
|
196
|
-
pendingStore: getConfirmStore(),
|
|
197
|
-
});
|
|
198
|
-
const handled = await adapter.handleCallbackQuery(callbackQuery);
|
|
199
|
-
if (!handled) {
|
|
200
|
-
this.log(`telegram[${this.channel.name}] unhandled callback_query: ${callbackQuery.data}`);
|
|
201
|
-
}
|
|
169
|
+
return handleCallbackQuery(this, callbackQuery);
|
|
202
170
|
}
|
|
203
171
|
|
|
204
|
-
// ── ask_questions: state-machine helpers ───────────────────────────────
|
|
205
|
-
// The flow lives in telegram-ask.js; this class owns the I/O (sending
|
|
206
|
-
// messages, editing keyboards, re-entering the super-agent loop with the
|
|
207
|
-
// compiled answer once the flow finishes).
|
|
208
|
-
|
|
209
|
-
async _renderQuestion(state) {
|
|
210
|
-
const text = askFlow.formatQuestionText(state);
|
|
211
|
-
const reply_markup = askFlow.buildKeyboard(state);
|
|
212
|
-
// If we already have a message for the previous question, leave its
|
|
213
|
-
// keyboard wiped — we draw a fresh message per question for clearer
|
|
214
|
-
// history in the chat (the question text stays as a record).
|
|
215
|
-
if (state.messageId) {
|
|
216
|
-
try {
|
|
217
|
-
await this._editKeyboard({
|
|
218
|
-
chat_id: state.chatId,
|
|
219
|
-
message_id: state.messageId,
|
|
220
|
-
reply_markup: { inline_keyboard: [] },
|
|
221
|
-
});
|
|
222
|
-
} catch { /* best-effort */ }
|
|
223
|
-
}
|
|
224
|
-
const sent = await this._send({
|
|
225
|
-
chat_id: state.chatId,
|
|
226
|
-
text,
|
|
227
|
-
reply_markup,
|
|
228
|
-
parse_mode: "Markdown",
|
|
229
|
-
});
|
|
230
|
-
state.messageId = sent?.message_id || null;
|
|
231
|
-
askFlow.saveState(state.chatId, state);
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// Kick off a brand-new ask flow after the super-agent called ask_questions.
|
|
235
|
-
// The flow's `resume` callback captures the per-turn context (sender,
|
|
236
|
-
// relationship, project) so when the compiled answer arrives we can run
|
|
237
|
-
// another super-agent turn without retyping all the inputs.
|
|
238
172
|
async _startAskFlow(ctx) {
|
|
239
|
-
|
|
240
|
-
chatId: ctx.chat_id,
|
|
241
|
-
projectId: ctx.projectId,
|
|
242
|
-
authorId: ctx.authorId,
|
|
243
|
-
questions: ctx.questions,
|
|
244
|
-
resume: async (compiled) => {
|
|
245
|
-
await this._runResumedTurn({ ...ctx, compiled });
|
|
246
|
-
},
|
|
247
|
-
});
|
|
248
|
-
await this._renderQuestion(state);
|
|
173
|
+
return startAskFlow(this, ctx);
|
|
249
174
|
}
|
|
250
175
|
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
const chatId = callbackQuery.message?.chat?.id;
|
|
254
|
-
if (!chatId) return;
|
|
255
|
-
const result = askFlow.applyCallback(chatId, callbackQuery.data || "");
|
|
256
|
-
// Ack the press regardless — keeps the spinner from hanging client-side.
|
|
257
|
-
await this._answerCallback({ callback_query_id: callbackQuery.id });
|
|
258
|
-
if (!result) return; // stale or unknown — adapter already ack'd.
|
|
259
|
-
|
|
260
|
-
if (result.action === "redraw") {
|
|
261
|
-
// Multi-select toggle: just refresh the keyboard on the SAME message.
|
|
262
|
-
try {
|
|
263
|
-
await this._editKeyboard({
|
|
264
|
-
chat_id: chatId,
|
|
265
|
-
message_id: callbackQuery.message?.message_id,
|
|
266
|
-
reply_markup: askFlow.buildKeyboard(result.state),
|
|
267
|
-
});
|
|
268
|
-
} catch (e) {
|
|
269
|
-
this.log(`telegram[${this.channel.name}] redraw failed: ${e.message}`);
|
|
270
|
-
}
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
if (result.action === "advance") {
|
|
274
|
-
await this._renderQuestion(result.state);
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
if (result.action === "cancel") {
|
|
278
|
-
try {
|
|
279
|
-
await this._editKeyboard({
|
|
280
|
-
chat_id: chatId,
|
|
281
|
-
message_id: callbackQuery.message?.message_id,
|
|
282
|
-
reply_markup: { inline_keyboard: [] },
|
|
283
|
-
});
|
|
284
|
-
await this._send({ chat_id: chatId, text: "Pregunta cancelada." });
|
|
285
|
-
} catch { /* best-effort */ }
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
if (result.action === "done") {
|
|
289
|
-
try {
|
|
290
|
-
await this._editKeyboard({
|
|
291
|
-
chat_id: chatId,
|
|
292
|
-
message_id: callbackQuery.message?.message_id,
|
|
293
|
-
reply_markup: { inline_keyboard: [] },
|
|
294
|
-
});
|
|
295
|
-
} catch { /* best-effort */ }
|
|
296
|
-
// Feed the compiled answer back as a synthetic user turn.
|
|
297
|
-
if (typeof result.state.resume === "function") {
|
|
298
|
-
await result.state.resume(result.compiled);
|
|
299
|
-
}
|
|
300
|
-
}
|
|
176
|
+
async _maybeConsumeAskTextAnswer(args) {
|
|
177
|
+
return maybeConsumeAskTextAnswer(this, args);
|
|
301
178
|
}
|
|
302
179
|
|
|
303
|
-
//
|
|
304
|
-
//
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
if (!
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
// Advance: emit a synthetic "next" to move past this question.
|
|
312
|
-
const next = askFlow.applyCallback(
|
|
313
|
-
chat_id,
|
|
314
|
-
`apx:ask:${state.correlationId}:next`,
|
|
315
|
-
);
|
|
316
|
-
if (!next) return true;
|
|
317
|
-
if (next.action === "advance") {
|
|
318
|
-
await this._renderQuestion(next.state);
|
|
319
|
-
return true;
|
|
320
|
-
}
|
|
321
|
-
if (next.action === "done") {
|
|
322
|
-
if (typeof next.state.resume === "function") {
|
|
323
|
-
await next.state.resume(next.compiled);
|
|
324
|
-
}
|
|
325
|
-
return true;
|
|
326
|
-
}
|
|
327
|
-
return true;
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// Run a follow-up super-agent turn with the compiled answers as the user
|
|
331
|
-
// prompt. Mirrors the post-runSuperAgent reply path in _handleUpdate but
|
|
332
|
-
// skipped of the photo/audio/reset preamble. Re-enters the ask flow if the
|
|
333
|
-
// model decides to ask again.
|
|
334
|
-
async _runResumedTurn(ctx) {
|
|
335
|
-
const { chat_id, compiled, target, relationshipBlock, allowedTools, author, agentDisplay, update_id, sender, authorId } = ctx;
|
|
336
|
-
if (!chat_id) return;
|
|
337
|
-
// Log the synthetic user message so getRecentTelegramTurnsFromFs picks
|
|
338
|
-
// it up on the NEXT inbound. Mirrors how a normal text reply would be
|
|
339
|
-
// recorded.
|
|
340
|
-
appendGlobalMessage({
|
|
341
|
-
channel: CHANNELS.TELEGRAM,
|
|
342
|
-
direction: "in",
|
|
343
|
-
type: "user",
|
|
344
|
-
actor_id: authorId ? String(authorId) : (author || "ask_flow"),
|
|
345
|
-
external_id: `ask-${Date.now()}`,
|
|
346
|
-
author: author || "user",
|
|
347
|
-
body: compiled,
|
|
348
|
-
meta: {
|
|
349
|
-
chat_id,
|
|
350
|
-
user_id: authorId || null,
|
|
351
|
-
tg_channel: this.channel.name,
|
|
352
|
-
ask_flow: true,
|
|
353
|
-
},
|
|
354
|
-
});
|
|
355
|
-
|
|
356
|
-
const previousMessages = getRecentTelegramTurnsFromFs({
|
|
357
|
-
chat_id,
|
|
358
|
-
keepRecent: 40,
|
|
359
|
-
max_age_hours: 24,
|
|
360
|
-
});
|
|
361
|
-
|
|
362
|
-
const stopTyping = this._startTyping(chat_id);
|
|
363
|
-
try {
|
|
364
|
-
const sa = await runSuperAgent({
|
|
365
|
-
globalConfig: this.globalConfig,
|
|
366
|
-
projects: this.projects,
|
|
367
|
-
plugins: this.plugins,
|
|
368
|
-
registries: this.registries,
|
|
369
|
-
prompt: compiled,
|
|
370
|
-
previousMessages,
|
|
371
|
-
channel: CHANNELS.TELEGRAM,
|
|
372
|
-
relationshipBlock,
|
|
373
|
-
allowedTools,
|
|
374
|
-
channelMeta: { channel: CHANNELS.TELEGRAM, chat_id, author, route_to_agent: this.channel.route_to_agent },
|
|
375
|
-
});
|
|
376
|
-
stopTyping();
|
|
377
|
-
|
|
378
|
-
// Did the model ask again? Restart the flow instead of replying.
|
|
379
|
-
const followupAsk = askFlow.extractAskQuestionsFromTrace(sa.trace);
|
|
380
|
-
if (followupAsk) {
|
|
381
|
-
await this._startAskFlow({
|
|
382
|
-
chat_id,
|
|
383
|
-
projectId: target?.id,
|
|
384
|
-
authorId,
|
|
385
|
-
questions: followupAsk,
|
|
386
|
-
author,
|
|
387
|
-
agentDisplay,
|
|
388
|
-
relationshipBlock,
|
|
389
|
-
allowedTools,
|
|
390
|
-
target,
|
|
391
|
-
sender,
|
|
392
|
-
update_id,
|
|
393
|
-
});
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
const replyText = sa.text ? stripThinking(sa.text).trim() : "";
|
|
398
|
-
if (replyText) {
|
|
399
|
-
await this._send({ chat_id, text: replyText });
|
|
400
|
-
appendGlobalMessage({
|
|
401
|
-
channel: CHANNELS.TELEGRAM,
|
|
402
|
-
direction: "out",
|
|
403
|
-
type: "agent",
|
|
404
|
-
actor_id: SUPERAGENT_ACTOR_ID,
|
|
405
|
-
actor_kind: "superagent",
|
|
406
|
-
agent_slug: SUPERAGENT_ACTOR_ID,
|
|
407
|
-
author: sa.name || agentDisplay,
|
|
408
|
-
body: replyText,
|
|
409
|
-
meta: {
|
|
410
|
-
chat_id,
|
|
411
|
-
tg_channel: this.channel.name,
|
|
412
|
-
in_reply_to: update_id,
|
|
413
|
-
final: true,
|
|
414
|
-
ask_resume: true,
|
|
415
|
-
...(sa.usage ? { usage: sa.usage } : {}),
|
|
416
|
-
},
|
|
417
|
-
});
|
|
418
|
-
}
|
|
419
|
-
} catch (e) {
|
|
420
|
-
stopTyping();
|
|
421
|
-
this.log(`telegram[${this.channel.name}] ask resume failed: ${e.message}`);
|
|
422
|
-
try {
|
|
423
|
-
await this._send({ chat_id, text: `⚠️ Error procesando tus respuestas (${e.message}).` });
|
|
424
|
-
} catch { /* best-effort */ }
|
|
425
|
-
}
|
|
180
|
+
// Resolve the bot token + outbound chat for this channel — the single place
|
|
181
|
+
// the "no token / no chat" guards live, shared by every send method.
|
|
182
|
+
_resolve(chat_id) {
|
|
183
|
+
const token = resolveBotToken(this.channel);
|
|
184
|
+
if (!token) throw new Error(`channel ${this.channel.name}: no bot_token`);
|
|
185
|
+
const target = chat_id || resolveChatId(this.channel);
|
|
186
|
+
if (!target) throw new Error(`channel ${this.channel.name}: no chat_id`);
|
|
187
|
+
return { token, target };
|
|
426
188
|
}
|
|
427
189
|
|
|
428
|
-
// Show "typing..." indicator
|
|
429
|
-
//
|
|
190
|
+
// Show "typing..." indicator. Telegram clears it after ~5s; _startTyping
|
|
191
|
+
// re-pings every 4s. Best-effort — failures aren't worth surfacing.
|
|
430
192
|
async _typing(chat_id) {
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
const url = `${API_BASE}/bot${token}/sendChatAction`;
|
|
435
|
-
await fetch(url, {
|
|
436
|
-
method: "POST",
|
|
437
|
-
headers: { "content-type": "application/json" },
|
|
438
|
-
body: JSON.stringify({ chat_id, action: "typing" }),
|
|
439
|
-
});
|
|
440
|
-
} catch {
|
|
441
|
-
// best-effort; failures here aren't worth surfacing
|
|
442
|
-
}
|
|
193
|
+
const token = resolveBotToken(this.channel);
|
|
194
|
+
if (!token || !chat_id) return;
|
|
195
|
+
try { await sendChatAction(token, chat_id); } catch { /* best-effort */ }
|
|
443
196
|
}
|
|
444
197
|
|
|
445
|
-
// Returns a
|
|
446
|
-
// stop(). Used to wrap the engine round-trip in a "typing" loop so the
|
|
447
|
-
// user sees feedback while qwen thinks.
|
|
198
|
+
// Returns a stop() fn; pings the typing indicator every 4s until called.
|
|
448
199
|
_startTyping(chat_id) {
|
|
449
200
|
if (!chat_id) return () => {};
|
|
450
201
|
let stopped = false;
|
|
@@ -458,58 +209,27 @@ class ChannelPoller {
|
|
|
458
209
|
}
|
|
459
210
|
|
|
460
211
|
async _send({ chat_id, text, reply_markup, parse_mode }) {
|
|
461
|
-
const token =
|
|
462
|
-
|
|
463
|
-
const target = chat_id || resolveChatId(this.channel);
|
|
464
|
-
if (!target) throw new Error(`channel ${this.channel.name}: no chat_id`);
|
|
465
|
-
const url = `${API_BASE}/bot${token}/sendMessage`;
|
|
466
|
-
const body = { chat_id: target, text };
|
|
467
|
-
if (reply_markup) body.reply_markup = reply_markup;
|
|
468
|
-
if (parse_mode) body.parse_mode = parse_mode;
|
|
469
|
-
const res = await fetch(url, {
|
|
470
|
-
method: "POST",
|
|
471
|
-
headers: { "content-type": "application/json" },
|
|
472
|
-
body: JSON.stringify(body),
|
|
473
|
-
});
|
|
474
|
-
const json = await res.json();
|
|
475
|
-
if (!json.ok) throw new Error(json.description || `send failed (${res.status})`);
|
|
476
|
-
return json.result;
|
|
212
|
+
const { token, target } = this._resolve(chat_id);
|
|
213
|
+
return sendMessage(token, target, { text, reply_markup, parse_mode });
|
|
477
214
|
}
|
|
478
215
|
|
|
479
|
-
// Replace
|
|
480
|
-
// refresh after a multi-select toggle, or to wipe buttons once the flow
|
|
481
|
-
// has moved on). Best-effort: failures are logged but don't break the flow.
|
|
216
|
+
// Replace/clear the inline keyboard on a sent message. Best-effort: logged.
|
|
482
217
|
async _editKeyboard({ chat_id, message_id, reply_markup }) {
|
|
483
218
|
const token = resolveBotToken(this.channel);
|
|
484
219
|
if (!token) return;
|
|
485
220
|
try {
|
|
486
|
-
|
|
487
|
-
const body = { chat_id, message_id };
|
|
488
|
-
if (reply_markup) body.reply_markup = reply_markup;
|
|
489
|
-
await fetch(url, {
|
|
490
|
-
method: "POST",
|
|
491
|
-
headers: { "content-type": "application/json" },
|
|
492
|
-
body: JSON.stringify(body),
|
|
493
|
-
});
|
|
221
|
+
await editMessageReplyMarkup(token, chat_id, message_id, reply_markup);
|
|
494
222
|
} catch (e) {
|
|
495
223
|
this.log(`telegram[${this.channel.name}] editMessageReplyMarkup failed: ${e.message}`);
|
|
496
224
|
}
|
|
497
225
|
}
|
|
498
226
|
|
|
499
|
-
//
|
|
500
|
-
// the spinner on the tapped button. Optional `text` shows a small toast.
|
|
227
|
+
// Ack a callback button press so the client clears the spinner (+ optional toast).
|
|
501
228
|
async _answerCallback({ callback_query_id, text }) {
|
|
502
229
|
const token = resolveBotToken(this.channel);
|
|
503
230
|
if (!token) return;
|
|
504
231
|
try {
|
|
505
|
-
|
|
506
|
-
const body = { callback_query_id };
|
|
507
|
-
if (text) body.text = text;
|
|
508
|
-
await fetch(url, {
|
|
509
|
-
method: "POST",
|
|
510
|
-
headers: { "content-type": "application/json" },
|
|
511
|
-
body: JSON.stringify(body),
|
|
512
|
-
});
|
|
232
|
+
await answerCallbackQuery(token, callback_query_id, text);
|
|
513
233
|
} catch (e) {
|
|
514
234
|
this.log(`telegram[${this.channel.name}] answerCallbackQuery failed: ${e.message}`);
|
|
515
235
|
}
|
|
@@ -517,40 +237,42 @@ class ChannelPoller {
|
|
|
517
237
|
|
|
518
238
|
/** Send a photo via this channel */
|
|
519
239
|
async _sendPhoto({ chat_id, photo, caption, parse_mode }) {
|
|
520
|
-
const token =
|
|
521
|
-
if (!token) throw new Error(`channel ${this.channel.name}: no bot_token`);
|
|
522
|
-
const target = chat_id || resolveChatId(this.channel);
|
|
523
|
-
if (!target) throw new Error(`channel ${this.channel.name}: no chat_id`);
|
|
240
|
+
const { token, target } = this._resolve(chat_id);
|
|
524
241
|
return sendPhoto(token, target, photo, { caption, parse_mode });
|
|
525
242
|
}
|
|
526
243
|
|
|
527
244
|
/** Send a voice message via this channel */
|
|
528
245
|
async _sendVoice({ chat_id, audio, caption, duration }) {
|
|
529
|
-
const token =
|
|
530
|
-
if (!token) throw new Error(`channel ${this.channel.name}: no bot_token`);
|
|
531
|
-
const target = chat_id || resolveChatId(this.channel);
|
|
246
|
+
const { token, target } = this._resolve(chat_id);
|
|
532
247
|
return sendVoice(token, target, audio, { caption, duration });
|
|
533
248
|
}
|
|
534
249
|
|
|
535
250
|
/** Send a document (PDF, zip, etc) via this channel */
|
|
536
251
|
async _sendDocument({ chat_id, document, caption, filename, mime_type }) {
|
|
537
|
-
const token =
|
|
538
|
-
if (!token) throw new Error(`channel ${this.channel.name}: no bot_token`);
|
|
539
|
-
const target = chat_id || resolveChatId(this.channel);
|
|
252
|
+
const { token, target } = this._resolve(chat_id);
|
|
540
253
|
return sendDocument(token, target, document, { caption, filename, mime_type });
|
|
541
254
|
}
|
|
542
255
|
|
|
543
256
|
/** Send an audio file via this channel */
|
|
544
257
|
async _sendAudio({ chat_id, audio, caption, title, performer }) {
|
|
545
|
-
const token =
|
|
546
|
-
if (!token) throw new Error(`channel ${this.channel.name}: no bot_token`);
|
|
547
|
-
const target = chat_id || resolveChatId(this.channel);
|
|
258
|
+
const { token, target } = this._resolve(chat_id);
|
|
548
259
|
return sendAudio(token, target, audio, { caption, title, performer });
|
|
549
260
|
}
|
|
550
261
|
}
|
|
551
262
|
|
|
552
263
|
// ---------- plugin export ---------------------------------------------------
|
|
553
264
|
|
|
265
|
+
// Pick the poller to send through: the named channel if given, else the first
|
|
266
|
+
// channel with a usable bot token. Shared by every outbound helper below.
|
|
267
|
+
function pickPoller(pollers, channelName) {
|
|
268
|
+
const p =
|
|
269
|
+
(channelName && pollers.find((pp) => pp.channel.name === channelName)) ||
|
|
270
|
+
pollers.find((pp) => resolveBotToken(pp.channel)) ||
|
|
271
|
+
null;
|
|
272
|
+
if (!p) throw new Error("no telegram channel available");
|
|
273
|
+
return p;
|
|
274
|
+
}
|
|
275
|
+
|
|
554
276
|
export default {
|
|
555
277
|
id: "telegram",
|
|
556
278
|
|
|
@@ -590,11 +312,7 @@ export default {
|
|
|
590
312
|
// the outbound on `messages` of the channel's target project so audit
|
|
591
313
|
// trails are complete.
|
|
592
314
|
async send({ channel: channelName, chat_id, text, author = resolveAgentName(config), project }) {
|
|
593
|
-
const p =
|
|
594
|
-
(channelName && pollers.find((pp) => pp.channel.name === channelName)) ||
|
|
595
|
-
pollers.find((pp) => resolveBotToken(pp.channel)) ||
|
|
596
|
-
null;
|
|
597
|
-
if (!p) throw new Error("no telegram channel available");
|
|
315
|
+
const p = pickPoller(pollers, channelName);
|
|
598
316
|
const result = await p._send({ chat_id, text });
|
|
599
317
|
appendGlobalMessage({
|
|
600
318
|
channel: CHANNELS.TELEGRAM,
|
|
@@ -620,11 +338,7 @@ export default {
|
|
|
620
338
|
* opts: { caption, parse_mode, channel, author }
|
|
621
339
|
*/
|
|
622
340
|
async sendPhoto({ channel: channelName, chat_id, photo, caption, parse_mode, author = resolveAgentName(config) }) {
|
|
623
|
-
const p =
|
|
624
|
-
(channelName && pollers.find((pp) => pp.channel.name === channelName)) ||
|
|
625
|
-
pollers.find((pp) => resolveBotToken(pp.channel)) ||
|
|
626
|
-
null;
|
|
627
|
-
if (!p) throw new Error("no telegram channel available");
|
|
341
|
+
const p = pickPoller(pollers, channelName);
|
|
628
342
|
const result = await p._sendPhoto({ chat_id, photo, caption, parse_mode });
|
|
629
343
|
appendGlobalMessage({
|
|
630
344
|
channel: CHANNELS.TELEGRAM,
|
|
@@ -644,11 +358,7 @@ export default {
|
|
|
644
358
|
* audio: local file path or Buffer
|
|
645
359
|
*/
|
|
646
360
|
async sendVoice({ channel: channelName, chat_id, audio, caption, duration, author = resolveAgentName(config) }) {
|
|
647
|
-
const p =
|
|
648
|
-
(channelName && pollers.find((pp) => pp.channel.name === channelName)) ||
|
|
649
|
-
pollers.find((pp) => resolveBotToken(pp.channel)) ||
|
|
650
|
-
null;
|
|
651
|
-
if (!p) throw new Error("no telegram channel available");
|
|
361
|
+
const p = pickPoller(pollers, channelName);
|
|
652
362
|
const result = await p._sendVoice({ chat_id, audio, caption, duration });
|
|
653
363
|
appendGlobalMessage({
|
|
654
364
|
channel: CHANNELS.TELEGRAM,
|
|
@@ -668,11 +378,7 @@ export default {
|
|
|
668
378
|
* document: local file path, Buffer, or public https URL.
|
|
669
379
|
*/
|
|
670
380
|
async sendDocument({ channel: channelName, chat_id, document, caption, filename, mime_type, author = resolveAgentName(config) }) {
|
|
671
|
-
const p =
|
|
672
|
-
(channelName && pollers.find((pp) => pp.channel.name === channelName)) ||
|
|
673
|
-
pollers.find((pp) => resolveBotToken(pp.channel)) ||
|
|
674
|
-
null;
|
|
675
|
-
if (!p) throw new Error("no telegram channel available");
|
|
381
|
+
const p = pickPoller(pollers, channelName);
|
|
676
382
|
const result = await p._sendDocument({ chat_id, document, caption, filename, mime_type });
|
|
677
383
|
appendGlobalMessage({
|
|
678
384
|
channel: CHANNELS.TELEGRAM,
|
|
@@ -692,11 +398,7 @@ export default {
|
|
|
692
398
|
* audio: local file path or Buffer
|
|
693
399
|
*/
|
|
694
400
|
async sendAudio({ channel: channelName, chat_id, audio, caption, title, performer, author = resolveAgentName(config) }) {
|
|
695
|
-
const p =
|
|
696
|
-
(channelName && pollers.find((pp) => pp.channel.name === channelName)) ||
|
|
697
|
-
pollers.find((pp) => resolveBotToken(pp.channel)) ||
|
|
698
|
-
null;
|
|
699
|
-
if (!p) throw new Error("no telegram channel available");
|
|
401
|
+
const p = pickPoller(pollers, channelName);
|
|
700
402
|
const result = await p._sendAudio({ chat_id, audio, caption, title, performer });
|
|
701
403
|
appendGlobalMessage({
|
|
702
404
|
channel: CHANNELS.TELEGRAM,
|
|
@@ -3327,9 +3327,9 @@
|
|
|
3327
3327
|
}
|
|
3328
3328
|
},
|
|
3329
3329
|
"node_modules/postcss": {
|
|
3330
|
-
"version": "8.5.
|
|
3331
|
-
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.
|
|
3332
|
-
"integrity": "sha512-
|
|
3330
|
+
"version": "8.5.16",
|
|
3331
|
+
"resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.16.tgz",
|
|
3332
|
+
"integrity": "sha512-vuwillviilfKZsg0VGj5R/YwwcHx4SLsIOI/7K6mQkWx+l5cUHTjj5g0AasTBcyXsbfTgrwsUNmVUb5xVwyPwg==",
|
|
3333
3333
|
"funding": [
|
|
3334
3334
|
{
|
|
3335
3335
|
"type": "opencollective",
|