@llblab/pi-telegram 0.17.5 → 0.18.0

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.
Files changed (61) hide show
  1. package/AGENTS.md +67 -32
  2. package/BACKLOG.md +59 -19
  3. package/CHANGELOG.md +36 -15
  4. package/README.md +63 -35
  5. package/docs/README.md +3 -1
  6. package/docs/architecture.md +55 -23
  7. package/docs/callback-namespaces.md +1 -1
  8. package/docs/inbound.md +1 -1
  9. package/docs/locks.md +0 -2
  10. package/docs/multi-instance-bus.md +483 -0
  11. package/docs/outbound.md +4 -3
  12. package/docs/public-api.md +12 -10
  13. package/docs/sections.md +2 -2
  14. package/docs/ui-style.md +76 -0
  15. package/index.ts +789 -32
  16. package/lib/bindings.ts +68 -12
  17. package/lib/bus-api.ts +314 -0
  18. package/lib/bus-follower.ts +853 -0
  19. package/lib/bus-leader.ts +915 -0
  20. package/lib/bus.ts +866 -0
  21. package/lib/command-templates.ts +9 -11
  22. package/lib/commands.ts +133 -47
  23. package/lib/config.ts +53 -5
  24. package/lib/lifecycle.ts +23 -7
  25. package/lib/locks.ts +230 -66
  26. package/lib/media.ts +30 -2
  27. package/lib/menu-model.ts +48 -17
  28. package/lib/menu-queue.ts +51 -20
  29. package/lib/menu-settings.ts +9 -5
  30. package/lib/menu-status.ts +3 -0
  31. package/lib/menu-thinking.ts +3 -0
  32. package/lib/menu.ts +67 -26
  33. package/lib/outbound-attachments.ts +102 -17
  34. package/lib/outbound-buttons.ts +6 -2
  35. package/lib/outbound-voice.ts +31 -11
  36. package/lib/outbound.ts +6 -4
  37. package/lib/ownership.ts +119 -0
  38. package/lib/pi.ts +26 -3
  39. package/lib/polling.ts +477 -7
  40. package/lib/preview.ts +141 -88
  41. package/lib/prompt-templates.ts +3 -3
  42. package/lib/prompts.ts +80 -30
  43. package/lib/queue.ts +193 -91
  44. package/lib/rendering.ts +0 -25
  45. package/lib/replies.ts +187 -55
  46. package/lib/routing.ts +1673 -9
  47. package/lib/runtime-log.ts +123 -0
  48. package/lib/runtime.ts +84 -12
  49. package/lib/sections.ts +28 -21
  50. package/lib/setup.ts +1 -1
  51. package/lib/status.ts +532 -9
  52. package/lib/sync.ts +618 -0
  53. package/lib/target.ts +49 -0
  54. package/lib/telegram-api.ts +405 -40
  55. package/lib/text-groups.ts +5 -1
  56. package/lib/thread-reconciler.ts +915 -0
  57. package/lib/threads.ts +2205 -0
  58. package/lib/turns.ts +48 -3
  59. package/lib/updates.ts +355 -32
  60. package/package.json +24 -2
  61. package/docs/telegram-bot-api-rich-messages.md +0 -890
package/lib/bindings.ts CHANGED
@@ -21,6 +21,7 @@ import * as Replies from "./replies.ts";
21
21
  import * as Runtime from "./runtime.ts";
22
22
  import * as Setup from "./setup.ts";
23
23
  import * as Status from "./status.ts";
24
+ import * as Target from "./target.ts";
24
25
  import * as TelegramApi from "./telegram-api.ts";
25
26
 
26
27
  type ActivePiModel = NonNullable<Pi.ExtensionContext["model"]>;
@@ -37,10 +38,12 @@ type TelegramBridgeStatusUpdater =
37
38
  interface TelegramCommandsAndToolsBindingDeps {
38
39
  pi: Pi.ExtensionAPI;
39
40
  configStore: Config.TelegramConfigStore;
41
+ persistConfig: () => Promise<void>;
40
42
  setup: Setup.TelegramSetupGuard;
41
43
  activeTurnRuntime: Queue.TelegramActiveTurnStore<Queue.PendingTelegramTurn>;
42
44
  lockedPollingRuntime: Locks.TelegramLockedPollingRuntime<Pi.ExtensionContext>;
43
- getStatusLines: () => string[];
45
+ stopPolling?: () => Promise<void | string>;
46
+ getStatusLines: (options?: Status.TelegramBridgeStatusLineOptions) => string[];
44
47
  buttonActionStore: OutboundHandlers.TelegramButtonActionStore;
45
48
  sendMarkdownReply: (
46
49
  chatId: number,
@@ -50,6 +53,7 @@ interface TelegramCommandsAndToolsBindingDeps {
50
53
  ) => Promise<number | undefined>;
51
54
  callMultipart: OutboundHandlers.TelegramVoiceReplySenderDeps["sendMultipart"];
52
55
  getDefaultChatId: () => number | undefined;
56
+ getDefaultTarget?: () => OutboundAttachments.TelegramQueuedOutboundAttachmentTurnView["target"];
53
57
  canSendDirect: () => boolean;
54
58
  updateStatus: TelegramBridgeStatusUpdater;
55
59
  recordRuntimeEvent: TelegramRuntimeEventRecorder;
@@ -58,14 +62,17 @@ interface TelegramCommandsAndToolsBindingDeps {
58
62
  export function registerTelegramCommandsAndTools({
59
63
  pi,
60
64
  configStore,
65
+ persistConfig,
61
66
  setup,
62
67
  activeTurnRuntime,
63
68
  lockedPollingRuntime,
69
+ stopPolling,
64
70
  getStatusLines,
65
71
  buttonActionStore,
66
72
  sendMarkdownReply,
67
73
  callMultipart,
68
74
  getDefaultChatId,
75
+ getDefaultTarget,
69
76
  canSendDirect,
70
77
  updateStatus,
71
78
  recordRuntimeEvent,
@@ -73,27 +80,29 @@ export function registerTelegramCommandsAndTools({
73
80
  OutboundAttachments.registerTelegramOutboundAttachmentTool(pi, {
74
81
  getActiveTurn: activeTurnRuntime.get,
75
82
  getDefaultChatId,
83
+ getDefaultTarget,
76
84
  canSendDirect,
77
85
  sendMultipart: callMultipart,
78
86
  recordRuntimeEvent,
79
87
  });
80
88
  OutboundAttachments.registerTelegramOutboundMessageTool(pi, {
81
89
  getDefaultChatId,
90
+ getDefaultTarget,
82
91
  canSendDirect,
83
- planMessage: OutboundHandlers.createTelegramOutboundReplyPlanner(
84
- buttonActionStore,
85
- ),
92
+ planMessage:
93
+ OutboundHandlers.createTelegramOutboundReplyPlanner(buttonActionStore),
86
94
  sendMarkdownMessage: (chatId, markdown, options) =>
87
95
  sendMarkdownReply(chatId, undefined, markdown, options),
88
96
  recordRuntimeEvent,
89
97
  });
98
+ Prompts.registerTelegramHelpTool(pi);
90
99
  Commands.registerTelegramBridgeCommands(pi, {
91
100
  promptForConfig: Setup.createTelegramSetupPromptRuntime({
92
101
  getConfig: configStore.get,
93
102
  setConfig: configStore.set,
94
103
  setupGuard: setup,
95
104
  getMe: TelegramApi.fetchTelegramBotIdentity,
96
- persistConfig: configStore.persist,
105
+ persistConfig,
97
106
  startPolling: lockedPollingRuntime.start,
98
107
  updateStatus,
99
108
  recordRuntimeEvent,
@@ -102,7 +111,7 @@ export function registerTelegramCommandsAndTools({
102
111
  reloadConfig: configStore.load,
103
112
  hasBotToken: configStore.hasBotToken,
104
113
  startPolling: lockedPollingRuntime.start,
105
- stopPolling: lockedPollingRuntime.stop,
114
+ stopPolling: stopPolling ?? lockedPollingRuntime.stop,
106
115
  updateStatus,
107
116
  });
108
117
  }
@@ -115,7 +124,7 @@ interface TelegramLifecycleBindingDeps {
115
124
  >;
116
125
  configStore: Pick<
117
126
  Config.TelegramConfigStore,
118
- "getOutboundHandlers" | "hasBotToken"
127
+ "get" | "getOutboundHandlers" | "hasBotToken" | "load"
119
128
  >;
120
129
  abort: Runtime.TelegramRuntimeAbortPort;
121
130
  typing: Runtime.TelegramRuntimeTypingPort;
@@ -157,6 +166,14 @@ interface TelegramLifecycleBindingDeps {
157
166
  Keyboard.TelegramInlineKeyboardMarkup
158
167
  >["sendTextReply"] &
159
168
  NonNullable<OutboundHandlers.TelegramVoiceReplySenderDeps["sendTextReply"]>;
169
+ editInteractiveMessage: (
170
+ chatId: number,
171
+ messageId: number,
172
+ text: string,
173
+ mode: "html" | "markdown",
174
+ replyMarkup: Keyboard.TelegramInlineKeyboardMarkup,
175
+ ) => Promise<void>;
176
+ deleteMessage: (chatId: number, messageId: number) => Promise<void>;
160
177
  dispatchNextQueuedTelegramTurn: (ctx: Pi.ExtensionContext) => void;
161
178
  answerGuestQuery: NonNullable<
162
179
  Queue.TelegramAgentEndHookRuntimeDeps<
@@ -181,7 +198,9 @@ interface TelegramLifecycleBindingDeps {
181
198
  Keyboard.TelegramInlineKeyboardMarkup
182
199
  >["finalizeMarkdownPreview"];
183
200
  proactivePushChatIdGetter: () => number | undefined;
201
+ proactivePushTargetGetter: () => Queue.TelegramQueueTarget | undefined;
184
202
  isProactivePushEnabled: () => boolean;
203
+ canSendProactivePush: (ctx: Pi.ExtensionContext) => boolean;
185
204
  updateStatus: TelegramBridgeStatusUpdater;
186
205
  recordRuntimeEvent: TelegramRuntimeEventRecorder;
187
206
  }
@@ -206,12 +225,16 @@ export function registerTelegramLifecycleRuntimeHooks({
206
225
  sendRecordVoiceAction,
207
226
  sendMarkdownReply,
208
227
  sendTextReply,
228
+ editInteractiveMessage,
229
+ deleteMessage,
209
230
  dispatchNextQueuedTelegramTurn,
210
231
  answerGuestQuery,
211
232
  sendGuestReply,
212
233
  finalizeMarkdownPreview,
213
234
  proactivePushChatIdGetter,
235
+ proactivePushTargetGetter,
214
236
  isProactivePushEnabled,
237
+ canSendProactivePush,
215
238
  updateStatus,
216
239
  recordRuntimeEvent,
217
240
  }: TelegramLifecycleBindingDeps): void {
@@ -258,9 +281,15 @@ export function registerTelegramLifecycleRuntimeHooks({
258
281
  setFoldQueuedPromptsIntoHistory: lifecycle.setFoldQueuedPromptsIntoHistory,
259
282
  setActiveTurn: activeTurnRuntime.set,
260
283
  createPreviewState: previewRuntime.resetState,
261
- startTypingLoop: promptDispatchRuntime.startTypingLoop,
284
+ startTypingLoop: (ctx) => {
285
+ const turn = activeTurnRuntime.get();
286
+ promptDispatchRuntime.startTypingLoop(ctx, turn?.chatId, {
287
+ target: turn?.target,
288
+ });
289
+ },
262
290
  updateStatus,
263
291
  getActiveTurn: activeTurnRuntime.get,
292
+ loadConfig: configStore.load,
264
293
  extractAssistant: Replies.extractLatestAssistantMessageText,
265
294
  getFoldQueuedPromptsIntoHistory:
266
295
  lifecycle.shouldFoldQueuedPromptsIntoHistory,
@@ -269,6 +298,16 @@ export function registerTelegramLifecycleRuntimeHooks({
269
298
  dispatchNextQueuedTelegramTurn,
270
299
  requestDeferredDispatchNextQueuedTelegramTurn:
271
300
  deferredQueueDispatchRuntime.request,
301
+ scheduleActiveTurnDelivery(task) {
302
+ const timer = setTimeout(() => {
303
+ void task().catch((error) => {
304
+ recordRuntimeEvent("delivery", error, {
305
+ phase: "agent-end-background-delivery",
306
+ });
307
+ });
308
+ }, 0);
309
+ timer.unref?.();
310
+ },
272
311
  clearPreview: previewRuntime.clear,
273
312
  setPreviewPendingText: previewRuntime.setPendingText,
274
313
  finalizeMarkdownPreview,
@@ -280,8 +319,9 @@ export function registerTelegramLifecycleRuntimeHooks({
280
319
  planOutboundReply: outboundReplyPlanner,
281
320
  sendOutboundReplyArtifacts: outboundReplyArtifactSender,
282
321
  getDefaultChatId: proactivePushChatIdGetter,
322
+ getDefaultTarget: proactivePushTargetGetter,
283
323
  isProactivePushEnabled,
284
- canSendProactivePush: lockOwnershipGuard.ownsContext,
324
+ canSendProactivePush,
285
325
  recordRuntimeEvent,
286
326
  getActiveToolExecutions: lifecycle.getActiveToolExecutions,
287
327
  setActiveToolExecutions: lifecycle.setActiveToolExecutions,
@@ -291,11 +331,18 @@ export function registerTelegramLifecycleRuntimeHooks({
291
331
  const agentStartWithDedupReset = Lifecycle.createAgentStartDedupHook(
292
332
  agentLifecycleHooks.onAgentStart,
293
333
  );
334
+ const startActiveTurnTypingLoop = (ctx: Pi.ExtensionContext): void => {
335
+ const turn = activeTurnRuntime.get();
336
+ promptDispatchRuntime.startTypingLoop(ctx, turn?.chatId, {
337
+ target: turn?.target,
338
+ });
339
+ };
294
340
  const compactionObserver = Lifecycle.createTelegramCompactionObserverRuntime({
295
341
  setCompactionInProgress: lifecycle.setCompactionInProgress,
296
342
  updateStatus,
297
- startTypingLoop: promptDispatchRuntime.startTypingLoop,
343
+ startTypingLoop: startActiveTurnTypingLoop,
298
344
  stopTypingLoop: typing.stop,
345
+ shouldStartTypingLoop: activeTurnRuntime.has,
299
346
  requestDeferredDispatchNextQueuedTelegramTurn:
300
347
  deferredQueueDispatchRuntime.request,
301
348
  dispatchNextQueuedTelegramTurn,
@@ -304,11 +351,12 @@ export function registerTelegramLifecycleRuntimeHooks({
304
351
  const messageActivityTypingHooks =
305
352
  Lifecycle.createTelegramMessageActivityTypingHooks({
306
353
  hasActiveTurn: activeTurnRuntime.has,
307
- startTypingLoop: promptDispatchRuntime.startTypingLoop,
354
+ startTypingLoop: startActiveTurnTypingLoop,
308
355
  onMessageStart: previewRuntime.onMessageStart,
309
356
  onMessageUpdate: previewRuntime.onMessageUpdate,
310
357
  recordRuntimeEvent,
311
358
  });
359
+ const messageActivityHooks = messageActivityTypingHooks;
312
360
  Lifecycle.registerTelegramLifecycleHooks(pi, {
313
361
  ...sessionLifecycleRuntime,
314
362
  ...agentLifecycleHooks,
@@ -319,11 +367,19 @@ export function registerTelegramLifecycleRuntimeHooks({
319
367
  onSessionBeforeCompact: compactionObserver.onSessionBeforeCompact,
320
368
  onSessionCompact: compactionObserver.onSessionCompact,
321
369
  onAgentStart: agentStartWithDedupReset,
370
+ async onToolExecutionStart(event, _ctx) {
371
+ agentLifecycleHooks.onToolExecutionStart();
372
+ },
373
+ onToolExecutionUpdate() {},
374
+ async onToolExecutionEnd(event, ctx) {
375
+ agentLifecycleHooks.onToolExecutionEnd(event, ctx);
376
+ },
377
+ onAgentEnd: agentLifecycleHooks.onAgentEnd,
322
378
  onBeforeAgentStart: Prompts.createTelegramProactiveBeforeAgentStartHook({
323
379
  isConfigured: configStore.hasBotToken,
324
380
  isProactivePushEnabled,
325
381
  isCurrentOwner: lockOwnershipGuard.ownsContext,
326
382
  }),
327
- ...messageActivityTypingHooks,
383
+ ...messageActivityHooks,
328
384
  });
329
385
  }
package/lib/bus-api.ts ADDED
@@ -0,0 +1,314 @@
1
+ /**
2
+ * Telegram bus-aware API runtime
3
+ * Zones: multi-instance bus, telegram api transport, live instance routing
4
+ * Wraps the direct Telegram Bot API runtime so follower instances can route outbound calls through the bus leader
5
+ */
6
+
7
+ import type {
8
+ TelegramApiCallOptions,
9
+ TelegramBridgeApiRuntime,
10
+ TelegramEditMessageTextBody,
11
+ TelegramInputRichMessage,
12
+ TelegramSendMessageBody,
13
+ TelegramSendMessageDraftBody,
14
+ TelegramSendRichMessageBody,
15
+ TelegramSendRichMessageDraftBody,
16
+ TelegramSentMessage,
17
+ TelegramUpdate,
18
+ } from "./telegram-api.ts";
19
+
20
+ export type TelegramBusApiCall = (
21
+ method: string,
22
+ args: unknown[],
23
+ ) => Promise<unknown>;
24
+
25
+ export interface TelegramBusAwareApiRuntimeDeps {
26
+ directRuntime: TelegramBridgeApiRuntime;
27
+ ownsDirect: () => boolean;
28
+ callFollowerApi: TelegramBusApiCall;
29
+ getDefaultTarget?: () => { chatId: number; threadId?: number } | undefined;
30
+ }
31
+
32
+ function asRecord(value: unknown): Record<string, unknown> {
33
+ return value && typeof value === "object" && !Array.isArray(value)
34
+ ? (value as Record<string, unknown>)
35
+ : {};
36
+ }
37
+
38
+ function asBoolean(value: unknown): boolean {
39
+ return Boolean(value);
40
+ }
41
+
42
+ function asSentMessage(value: unknown): TelegramSentMessage {
43
+ return asRecord(value) as unknown as TelegramSentMessage;
44
+ }
45
+
46
+ function withDefaultThreadTarget(
47
+ body: Record<string, unknown>,
48
+ target: { chatId: number; threadId?: number } | undefined,
49
+ ): Record<string, unknown> {
50
+ if (target?.threadId === undefined) return body;
51
+ if (body.message_thread_id !== undefined) return body;
52
+ return body.chat_id === target.chatId
53
+ ? { ...body, message_thread_id: target.threadId }
54
+ : body;
55
+ }
56
+
57
+ export function createTelegramAggregateTypingActionSender(
58
+ runtime: Pick<TelegramBridgeApiRuntime, "call">,
59
+ ): (chatId: number) => Promise<unknown> {
60
+ return (chatId) =>
61
+ runtime.call("sendChatAction", { chat_id: chatId, action: "typing" });
62
+ }
63
+
64
+ export function createTelegramBusAwareApiRuntime(
65
+ deps: TelegramBusAwareApiRuntimeDeps,
66
+ ): TelegramBridgeApiRuntime {
67
+ return {
68
+ call<TResponse>(
69
+ method: string,
70
+ body: Record<string, unknown>,
71
+ options?: TelegramApiCallOptions,
72
+ ): Promise<TResponse> {
73
+ return deps.ownsDirect()
74
+ ? deps.directRuntime.call<TResponse>(method, body, options)
75
+ : (deps.callFollowerApi("call", [
76
+ method,
77
+ body,
78
+ options,
79
+ ]) as Promise<TResponse>);
80
+ },
81
+ callMultipart<TResponse>(
82
+ method: string,
83
+ fields: Record<string, string>,
84
+ fileField: string,
85
+ filePath: string,
86
+ fileName: string,
87
+ options?: TelegramApiCallOptions,
88
+ ): Promise<TResponse> {
89
+ return deps.ownsDirect()
90
+ ? deps.directRuntime.callMultipart<TResponse>(
91
+ method,
92
+ fields,
93
+ fileField,
94
+ filePath,
95
+ fileName,
96
+ options,
97
+ )
98
+ : (deps.callFollowerApi("callMultipart", [
99
+ method,
100
+ fields,
101
+ fileField,
102
+ filePath,
103
+ fileName,
104
+ options,
105
+ ]) as Promise<TResponse>);
106
+ },
107
+ downloadFile(fileId: string, suggestedName: string): Promise<string> {
108
+ return deps.ownsDirect()
109
+ ? deps.directRuntime.downloadFile(fileId, suggestedName)
110
+ : (deps.callFollowerApi("downloadFile", [
111
+ fileId,
112
+ suggestedName,
113
+ ]) as Promise<string>);
114
+ },
115
+ deleteWebhook(signal?: AbortSignal): Promise<boolean> {
116
+ return deps.directRuntime.deleteWebhook(signal);
117
+ },
118
+ getUpdates(
119
+ body: Record<string, unknown>,
120
+ signal?: AbortSignal,
121
+ ): Promise<TelegramUpdate[]> {
122
+ return deps.directRuntime.getUpdates(body, signal);
123
+ },
124
+ setMyCommands(commands): Promise<boolean> {
125
+ return deps.ownsDirect()
126
+ ? deps.directRuntime.setMyCommands(commands)
127
+ : deps
128
+ .callFollowerApi("call", ["setMyCommands", { commands }])
129
+ .then(asBoolean);
130
+ },
131
+ sendChatAction(
132
+ chatId: number,
133
+ action: string,
134
+ options?: { message_thread_id?: number },
135
+ ): Promise<boolean> {
136
+ const body = withDefaultThreadTarget(
137
+ {
138
+ chat_id: chatId,
139
+ action,
140
+ ...(options?.message_thread_id !== undefined
141
+ ? { message_thread_id: options.message_thread_id }
142
+ : {}),
143
+ },
144
+ deps.getDefaultTarget?.(),
145
+ );
146
+ return deps.ownsDirect()
147
+ ? deps.directRuntime.sendChatAction(chatId, action, options)
148
+ : deps
149
+ .callFollowerApi("call", ["sendChatAction", body])
150
+ .then(asBoolean);
151
+ },
152
+ sendTypingAction(
153
+ chatId: number,
154
+ options?: { message_thread_id?: number },
155
+ ): Promise<unknown> {
156
+ const body = withDefaultThreadTarget(
157
+ {
158
+ chat_id: chatId,
159
+ action: "typing",
160
+ ...(options?.message_thread_id !== undefined
161
+ ? { message_thread_id: options.message_thread_id }
162
+ : {}),
163
+ },
164
+ deps.getDefaultTarget?.(),
165
+ );
166
+ return deps.ownsDirect()
167
+ ? deps.directRuntime.sendTypingAction(chatId, options)
168
+ : deps
169
+ .callFollowerApi("call", ["sendChatAction", body])
170
+ .then(asBoolean);
171
+ },
172
+ sendRecordVoiceAction(
173
+ chatId: number,
174
+ options?: { message_thread_id?: number },
175
+ ): Promise<unknown> {
176
+ const body = withDefaultThreadTarget(
177
+ {
178
+ chat_id: chatId,
179
+ action: "record_voice",
180
+ ...(options?.message_thread_id !== undefined
181
+ ? { message_thread_id: options.message_thread_id }
182
+ : {}),
183
+ },
184
+ deps.getDefaultTarget?.(),
185
+ );
186
+ return deps.ownsDirect()
187
+ ? deps.directRuntime.sendRecordVoiceAction(chatId, options)
188
+ : deps
189
+ .callFollowerApi("call", ["sendChatAction", body])
190
+ .then(asBoolean);
191
+ },
192
+ sendMessageDraft(
193
+ chatId: number,
194
+ draftId: number,
195
+ text?: string,
196
+ options?: TelegramSendMessageDraftBody extends Record<string, unknown>
197
+ ? {
198
+ parse_mode?: string;
199
+ entities?: unknown[];
200
+ message_thread_id?: number;
201
+ }
202
+ : never,
203
+ ): Promise<boolean> {
204
+ const body: Record<string, unknown> = {
205
+ chat_id: chatId,
206
+ draft_id: draftId,
207
+ };
208
+ if (text !== undefined) body.text = text;
209
+ if (options?.parse_mode !== undefined)
210
+ body.parse_mode = options.parse_mode;
211
+ if (options?.entities !== undefined) body.entities = options.entities;
212
+ if (options?.message_thread_id !== undefined) {
213
+ body.message_thread_id = options.message_thread_id;
214
+ }
215
+ const scopedBody = withDefaultThreadTarget(
216
+ body,
217
+ deps.getDefaultTarget?.(),
218
+ );
219
+ return deps.ownsDirect()
220
+ ? deps.directRuntime.sendMessageDraft(chatId, draftId, text, options)
221
+ : deps
222
+ .callFollowerApi("call", ["sendMessageDraft", scopedBody])
223
+ .then(asBoolean);
224
+ },
225
+ sendMessage(body: TelegramSendMessageBody): Promise<TelegramSentMessage> {
226
+ return deps.ownsDirect()
227
+ ? deps.directRuntime.sendMessage(body)
228
+ : deps
229
+ .callFollowerApi("call", ["sendMessage", body])
230
+ .then(asSentMessage);
231
+ },
232
+ sendRichMessage(
233
+ body: TelegramSendRichMessageBody,
234
+ ): Promise<TelegramSentMessage> {
235
+ return deps.ownsDirect()
236
+ ? deps.directRuntime.sendRichMessage(body)
237
+ : deps
238
+ .callFollowerApi("call", ["sendRichMessage", body])
239
+ .then(asSentMessage);
240
+ },
241
+ sendRichMessageDraft(
242
+ body: TelegramSendRichMessageDraftBody,
243
+ ): Promise<boolean> {
244
+ return deps.ownsDirect()
245
+ ? deps.directRuntime.sendRichMessageDraft(body)
246
+ : deps
247
+ .callFollowerApi("call", ["sendRichMessageDraft", body])
248
+ .then(asBoolean);
249
+ },
250
+ async editMessageText(
251
+ body: TelegramEditMessageTextBody,
252
+ ): Promise<"edited" | "unchanged"> {
253
+ if (deps.ownsDirect()) return deps.directRuntime.editMessageText(body);
254
+ await deps.callFollowerApi("call", ["editMessageText", body]);
255
+ return "edited";
256
+ },
257
+ async answerCallbackQuery(
258
+ callbackQueryId: string,
259
+ text?: string,
260
+ ): Promise<void> {
261
+ if (deps.ownsDirect()) {
262
+ await deps.directRuntime.answerCallbackQuery(callbackQueryId, text);
263
+ return;
264
+ }
265
+ await deps.callFollowerApi("call", [
266
+ "answerCallbackQuery",
267
+ {
268
+ callback_query_id: callbackQueryId,
269
+ ...(text !== undefined ? { text } : {}),
270
+ },
271
+ ]);
272
+ },
273
+ async answerGuestQuery(
274
+ guestQueryId: string,
275
+ text?: string,
276
+ options?: { parseMode?: string; richMessage?: TelegramInputRichMessage },
277
+ ): Promise<void> {
278
+ if (deps.ownsDirect()) {
279
+ await deps.directRuntime.answerGuestQuery(guestQueryId, text, options);
280
+ return;
281
+ }
282
+ const body: Record<string, unknown> = { guest_query_id: guestQueryId };
283
+ if (text !== undefined || options?.richMessage) {
284
+ const inputContent: Record<string, unknown> = options?.richMessage
285
+ ? { rich_message: options.richMessage }
286
+ : { message_text: text };
287
+ if (!options?.richMessage && options?.parseMode) {
288
+ inputContent.parse_mode = options.parseMode;
289
+ }
290
+ body.result = {
291
+ type: "article",
292
+ id: "1",
293
+ title: "Response",
294
+ input_message_content: inputContent,
295
+ };
296
+ }
297
+ await deps.callFollowerApi("call", ["answerGuestQuery", body]);
298
+ },
299
+ async deleteMessage(chatId: number, messageId: number): Promise<void> {
300
+ if (deps.ownsDirect())
301
+ return deps.directRuntime.deleteMessage(chatId, messageId);
302
+ await deps.callFollowerApi("call", [
303
+ "deleteMessage",
304
+ {
305
+ chat_id: chatId,
306
+ message_id: messageId,
307
+ },
308
+ ]);
309
+ },
310
+ prepareTempDir(): Promise<number> {
311
+ return deps.directRuntime.prepareTempDir();
312
+ },
313
+ };
314
+ }