@llblab/pi-telegram 0.10.8 → 0.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/config.ts CHANGED
@@ -12,6 +12,8 @@ import { join, resolve } from "node:path";
12
12
  import type { TelegramInboundHandlerConfig } from "./inbound-handlers.ts";
13
13
  import type { CommandTemplateObjectConfig } from "./command-templates.ts";
14
14
 
15
+ const CONFIG_RUNTIME_KEY = "__piTelegramConfigRuntime__";
16
+
15
17
  function getAgentDir(): string {
16
18
  return process.env.PI_CODING_AGENT_DIR
17
19
  ? resolve(process.env.PI_CODING_AGENT_DIR)
@@ -33,6 +35,19 @@ export interface TelegramOutboundHandlerConfig extends CommandTemplateObjectConf
33
35
  timeout?: number;
34
36
  }
35
37
 
38
+ export type TelegramTimeMode = "off" | "always" | "interval";
39
+
40
+ export interface TelegramTimeConfig {
41
+ injectionMode?: TelegramTimeMode;
42
+ interval?: number;
43
+ }
44
+
45
+ export interface ResolvedTelegramTimeConfig {
46
+ injectionMode: TelegramTimeMode;
47
+ interval: number;
48
+ timezone: string;
49
+ }
50
+
36
51
  export interface TelegramConfig {
37
52
  botToken?: string;
38
53
  botUsername?: string;
@@ -43,6 +58,12 @@ export interface TelegramConfig {
43
58
  attachmentHandlers?: TelegramInboundHandlerConfig[];
44
59
  outboundHandlers?: TelegramOutboundHandlerConfig[];
45
60
  proactivePush?: boolean;
61
+ voice?: {
62
+ replyMode?: "manual" | "mirror" | "always";
63
+ /** Whether to attach the provider's transcriptText as caption on voice messages */
64
+ sendTranscript?: boolean;
65
+ };
66
+ time?: TelegramTimeConfig;
46
67
  }
47
68
 
48
69
  export interface TelegramConfigStore {
@@ -66,6 +87,29 @@ export interface TelegramConfigStoreOptions {
66
87
  configPath?: string;
67
88
  }
68
89
 
90
+ export interface TelegramConfigRuntime {
91
+ updateVoiceConfig: (voice: NonNullable<TelegramConfig["voice"]>) => void;
92
+ }
93
+
94
+ export function setGlobalTelegramConfigRuntime(
95
+ runtime: TelegramConfigRuntime | undefined,
96
+ ): void {
97
+ const globals = globalThis as Record<string, unknown>;
98
+ if (runtime) globals[CONFIG_RUNTIME_KEY] = runtime;
99
+ else delete globals[CONFIG_RUNTIME_KEY];
100
+ }
101
+
102
+ export function updateTelegramVoiceConfig(
103
+ voice: NonNullable<TelegramConfig["voice"]>,
104
+ ): boolean {
105
+ const runtime = (globalThis as Record<string, unknown>)[
106
+ CONFIG_RUNTIME_KEY
107
+ ] as TelegramConfigRuntime | undefined;
108
+ if (!runtime || typeof runtime.updateVoiceConfig !== "function") return false;
109
+ runtime.updateVoiceConfig(voice);
110
+ return true;
111
+ }
112
+
69
113
  export async function readTelegramConfig(
70
114
  configPath: string,
71
115
  ): Promise<TelegramConfig> {
@@ -141,6 +185,96 @@ export function createTelegramProactivePushSetter(
141
185
  };
142
186
  }
143
187
 
188
+ export function createTelegramVoiceReplyModeGetter(
189
+ configStore: Pick<TelegramConfigStore, "get">,
190
+ ): () => "manual" | "mirror" | "always" {
191
+ return () => {
192
+ const mode = configStore.get().voice?.replyMode;
193
+ return mode === "mirror" || mode === "always" || mode === "manual"
194
+ ? mode
195
+ : "manual";
196
+ };
197
+ }
198
+
199
+ export function createTelegramVoiceReplyModeConfiguredChecker(
200
+ configStore: Pick<TelegramConfigStore, "get">,
201
+ ): () => boolean {
202
+ return () => {
203
+ const mode = configStore.get().voice?.replyMode;
204
+ return mode === "mirror" || mode === "always" || mode === "manual";
205
+ };
206
+ }
207
+
208
+ export function createTelegramVoiceReplyModeSetter(
209
+ configStore: Pick<TelegramConfigStore, "get" | "set" | "persist">,
210
+ ): (replyMode: "manual" | "mirror" | "always" | undefined) => Promise<void> {
211
+ return async (replyMode) => {
212
+ const current = configStore.get();
213
+ if (replyMode === undefined) {
214
+ const { replyMode: _replyMode, ...remainingVoice } = current.voice ?? {};
215
+ const next = { ...current };
216
+ if (Object.keys(remainingVoice).length > 0) next.voice = remainingVoice;
217
+ else delete next.voice;
218
+ configStore.set(next);
219
+ await configStore.persist(next);
220
+ return;
221
+ }
222
+ const next = { ...current, voice: { ...(current.voice ?? {}), replyMode } };
223
+ configStore.set(next);
224
+ await configStore.persist(next);
225
+ };
226
+ }
227
+
228
+ function getSystemTimezone(): string {
229
+ try {
230
+ const tz = Intl.DateTimeFormat().resolvedOptions().timeZone;
231
+ return tz && tz.length > 0 ? tz : "UTC";
232
+ } catch {
233
+ return "UTC";
234
+ }
235
+ }
236
+
237
+ export function resolveTelegramTimeConfig(
238
+ raw: TelegramTimeConfig | undefined,
239
+ ): ResolvedTelegramTimeConfig {
240
+ const injectionMode: TelegramTimeMode =
241
+ raw?.injectionMode === "always" || raw?.injectionMode === "interval"
242
+ ? raw.injectionMode
243
+ : "off";
244
+ const interval =
245
+ typeof raw?.interval === "number" && raw.interval > 0
246
+ ? raw.interval
247
+ : 60 * 60 * 1000;
248
+ const timezone = getSystemTimezone();
249
+ return { injectionMode, interval, timezone };
250
+ }
251
+
252
+ export function createTelegramTimeConfigGetter(
253
+ configStore: Pick<TelegramConfigStore, "get">,
254
+ ): () => ResolvedTelegramTimeConfig {
255
+ return () => resolveTelegramTimeConfig(configStore.get().time);
256
+ }
257
+
258
+ export function createTelegramTimeInjectionModeGetter(
259
+ configStore: Pick<TelegramConfigStore, "get">,
260
+ ): () => TelegramTimeMode {
261
+ return () => resolveTelegramTimeConfig(configStore.get().time).injectionMode;
262
+ }
263
+
264
+ export function createTelegramTimeInjectionModeSetter(
265
+ configStore: Pick<TelegramConfigStore, "get" | "set" | "persist">,
266
+ ): (injectionMode: TelegramTimeMode) => Promise<void> {
267
+ return async (injectionMode) => {
268
+ const current = configStore.get();
269
+ const next = {
270
+ ...current,
271
+ time: { ...(current.time ?? {}), injectionMode },
272
+ };
273
+ configStore.set(next);
274
+ await configStore.persist(next);
275
+ };
276
+ }
277
+
144
278
  export function createTelegramProactivePushChatIdGetter(deps: {
145
279
  getActiveTurnChatId: () => number | undefined;
146
280
  getAllowedUserId: () => number | undefined;
@@ -6,10 +6,17 @@
6
6
 
7
7
  import type { TelegramInlineKeyboardMarkup } from "./keyboard.ts";
8
8
 
9
+ const SECTION_REGISTRY_KEY = "__piTelegramSectionRegistry__";
10
+
9
11
  // --- Core Types ---
10
12
 
13
+ /** @internal */
11
14
  export type TelegramSectionId = string;
15
+
16
+ /** @internal */
12
17
  export type TelegramSectionToken = string;
18
+
19
+ /** @internal */
13
20
  export type TelegramSectionCallbackResult = "handled" | "pass";
14
21
 
15
22
  export interface TelegramSectionView {
@@ -34,6 +41,7 @@ export interface TelegramSectionRegistration {
34
41
  id: TelegramSectionId;
35
42
  label: string;
36
43
  order?: number;
44
+ getLabel?: () => string;
37
45
  render: (
38
46
  ctx: TelegramSectionContext,
39
47
  ) => TelegramSectionView | Promise<TelegramSectionView>;
@@ -71,6 +79,7 @@ export interface TelegramSectionCallbackContext {
71
79
  deleteMessage(): Promise<void>;
72
80
  }
73
81
 
82
+ /** @internal */
74
83
  export interface RegisteredTelegramSection {
75
84
  id: TelegramSectionId;
76
85
  token: TelegramSectionToken;
@@ -79,6 +88,7 @@ export interface RegisteredTelegramSection {
79
88
  registration: TelegramSectionRegistration;
80
89
  }
81
90
 
91
+ /** @internal */
82
92
  export interface TelegramSectionDiagnostic {
83
93
  id: TelegramSectionId;
84
94
  token: TelegramSectionToken;
@@ -87,6 +97,7 @@ export interface TelegramSectionDiagnostic {
87
97
  lastError?: string;
88
98
  }
89
99
 
100
+ /** @internal */
90
101
  export interface TelegramSectionRegistry {
91
102
  register(section: TelegramSectionRegistration): () => void;
92
103
  getSections(): RegisteredTelegramSection[];
@@ -97,11 +108,13 @@ export interface TelegramSectionRegistry {
97
108
  clear(): void;
98
109
  }
99
110
 
111
+ /** @internal */
100
112
  export interface TelegramSectionMainMenuRow {
101
113
  text: string;
102
114
  callback_data: string;
103
115
  }
104
116
 
117
+ /** @internal */
105
118
  export interface TelegramSectionSettingsRow {
106
119
  label: string;
107
120
  callback_data: string;
@@ -109,6 +122,7 @@ export interface TelegramSectionSettingsRow {
109
122
 
110
123
  // --- Runtime Port Builders ---
111
124
 
125
+ /** @internal */
112
126
  export interface TelegramSectionRuntimeDeps {
113
127
  answerCallbackQuery: (id: string, text?: string) => Promise<void>;
114
128
  editInteractiveMessage: (
@@ -229,17 +243,11 @@ function buildTelegramSectionCallbackContext(
229
243
 
230
244
  // --- GlobalThis Bridge ---
231
245
 
232
- const GLOBAL_SECTION_REGISTRY_KEY = "__piTelegramSectionRegistry__" as const;
233
-
234
- declare global {
235
- // eslint-disable-next-line no-var
236
- var __piTelegramSectionRegistry__: TelegramSectionRegistry | undefined;
237
- }
238
-
246
+ /** @internal */
239
247
  export function setGlobalTelegramSectionRegistry(
240
248
  registry: TelegramSectionRegistry,
241
249
  ): void {
242
- globalThis[GLOBAL_SECTION_REGISTRY_KEY] = registry;
250
+ (globalThis as Record<string, unknown>)[SECTION_REGISTRY_KEY] = registry;
243
251
  }
244
252
 
245
253
  /**
@@ -249,20 +257,26 @@ export function setGlobalTelegramSectionRegistry(
249
257
  export function registerTelegramSection(
250
258
  section: TelegramSectionRegistration,
251
259
  ): () => void {
252
- const registry = globalThis[GLOBAL_SECTION_REGISTRY_KEY];
260
+ const registry = (globalThis as Record<string, unknown>)[
261
+ SECTION_REGISTRY_KEY
262
+ ] as TelegramSectionRegistry | undefined;
253
263
  if (!registry) {
254
264
  throw new Error(
255
- "Telegram section registry not available. Is pi-telegram loaded?",
265
+ "Telegram section registry not available. Is pi-telegram loaded and initialized?",
256
266
  );
257
267
  }
268
+
258
269
  return registry.register(section);
259
270
  }
260
271
 
261
272
  /**
262
273
  * Get current section diagnostics. Returns empty array when registry is absent.
263
274
  */
275
+ /** @internal */
264
276
  export function getTelegramSectionDiagnostics(): TelegramSectionDiagnostic[] {
265
- const registry = globalThis[GLOBAL_SECTION_REGISTRY_KEY];
277
+ const registry = (globalThis as Record<string, unknown>)[
278
+ SECTION_REGISTRY_KEY
279
+ ] as TelegramSectionRegistry | undefined;
266
280
  return registry ? registry.getDiagnostics() : [];
267
281
  }
268
282
 
@@ -298,6 +312,7 @@ function prependBackRow(
298
312
  };
299
313
  }
300
314
 
315
+ /** @internal */
301
316
  export function createTelegramExtensionSectionRegistry(): TelegramSectionRegistry {
302
317
  const sections = new Map<TelegramSectionToken, RegisteredTelegramSection>();
303
318
  const errors = new Map<TelegramSectionToken, string>();
@@ -351,15 +366,7 @@ export function createTelegramExtensionSectionRegistry(): TelegramSectionRegistr
351
366
  return { register, getSections, getByToken, getDiagnostics, clear };
352
367
  }
353
368
 
354
- export function getTelegramSectionMainMenuRows(
355
- registry: TelegramSectionRegistry,
356
- ): TelegramSectionMainMenuRow[] {
357
- return registry.getSections().map((s) => ({
358
- text: s.label,
359
- callback_data: `section:${s.token}:open`,
360
- }));
361
- }
362
-
369
+ /** @internal */
363
370
  export function getTelegramExtensionSettingsRows(
364
371
  registry: TelegramSectionRegistry,
365
372
  ): TelegramSectionSettingsRow[] {
@@ -379,6 +386,17 @@ export function getTelegramExtensionSettingsRows(
379
386
  }));
380
387
  }
381
388
 
389
+ /** @internal */
390
+ export function getTelegramSectionMainMenuRows(
391
+ registry: TelegramSectionRegistry,
392
+ ): TelegramSectionMainMenuRow[] {
393
+ return registry.getSections().map((s) => ({
394
+ text: s.registration.getLabel?.() ?? s.label,
395
+ callback_data: `section:${s.token}:open`,
396
+ }));
397
+ }
398
+
399
+ /** @internal */
382
400
  export function parseTelegramSectionCallback(
383
401
  data: string,
384
402
  ): { token: string; action: string; payload: string } | undefined {
@@ -399,6 +417,7 @@ export function parseTelegramSectionCallback(
399
417
  };
400
418
  }
401
419
 
420
+ /** @internal */
402
421
  export interface TelegramSectionCallbackHandlerDeps {
403
422
  answerCallbackQuery: (id: string, text?: string) => Promise<void>;
404
423
  editInteractiveMessage: (
@@ -155,10 +155,9 @@ export function createTelegramExternalHandleUpdate<TUpdate, TContext>(
155
155
  * off();
156
156
  * ```
157
157
  *
158
- * Extensions that prefer zero coupling can also reach the registry directly
159
- * via `globalThis.__piTelegramExternalHandlerRegistry__` (versioned object,
160
- * see {@link TelegramExternalHandlerRegistry}). This avoids importing
161
- * `@llblab/pi-telegram` and tolerates either install order.
158
+ * Extensions that prefer zero coupling can also reach the versioned registry
159
+ * directly on `globalThis`. This avoids importing `@llblab/pi-telegram` and
160
+ * tolerates either install order.
162
161
  */
163
162
  export function onTelegramExternalUpdate(
164
163
  handler: TelegramExternalHandler,
@@ -14,8 +14,10 @@ import {
14
14
  type CommandTemplateConfig,
15
15
  type CommandTemplateObjectConfig,
16
16
  } from "./command-templates.ts";
17
+ import { getTelegramVoiceTranscriptionProviders } from "./voice.ts";
17
18
 
18
19
  const DEFAULT_INBOUND_HANDLER_TIMEOUT_MS = 120_000;
20
+ const INBOUND_HANDLER_REGISTRY_KEY = "__piTelegramInboundHandlers__";
19
21
 
20
22
  type TelegramInboundCommandTemplateConfig =
21
23
  | string
@@ -96,6 +98,27 @@ export interface TelegramInboundHandlerRuntime<TContext> {
96
98
  ) => Promise<TelegramInboundHandlerProcessResult<TFile>>;
97
99
  }
98
100
 
101
+ export type TelegramInboundProgrammaticHandlerResult =
102
+ | string
103
+ | { text: string }
104
+ | undefined;
105
+
106
+ export interface TelegramInboundProgrammaticHandlerInput {
107
+ kind: string;
108
+ text?: string;
109
+ file?: TelegramInboundHandlerFile;
110
+ mimeType?: string;
111
+ }
112
+
113
+ export type TelegramInboundProgrammaticHandler = (
114
+ input: TelegramInboundProgrammaticHandlerInput,
115
+ options?: { cwd?: string },
116
+ ) => Promise<TelegramInboundProgrammaticHandlerResult>;
117
+
118
+ export interface TelegramInboundHandlerRegistry {
119
+ handlers: Map<string, TelegramInboundProgrammaticHandler[]>;
120
+ }
121
+
99
122
  interface InboundHandlerInvocation {
100
123
  command: string;
101
124
  args: string[];
@@ -103,6 +126,74 @@ interface InboundHandlerInvocation {
103
126
 
104
127
  const BUILT_IN_TEXT_ATTACHMENT_MAX_BYTES = 1_000_000;
105
128
 
129
+ function getOrCreateInboundHandlerRegistry(): TelegramInboundHandlerRegistry {
130
+ const existing = (globalThis as Record<string, unknown>)[
131
+ INBOUND_HANDLER_REGISTRY_KEY
132
+ ];
133
+ if (
134
+ existing &&
135
+ typeof existing === "object" &&
136
+ existing !== null &&
137
+ "handlers" in existing &&
138
+ existing.handlers instanceof Map
139
+ ) {
140
+ return existing as TelegramInboundHandlerRegistry;
141
+ }
142
+ const registry: TelegramInboundHandlerRegistry = {
143
+ handlers: new Map(),
144
+ };
145
+ (globalThis as Record<string, unknown>)[INBOUND_HANDLER_REGISTRY_KEY] =
146
+ registry;
147
+ return registry;
148
+ }
149
+
150
+ export function registerTelegramInboundHandler(
151
+ kind: string,
152
+ handler: TelegramInboundProgrammaticHandler,
153
+ ): () => void {
154
+ const normalizedKind = kind.trim() || "*";
155
+ const registry = getOrCreateInboundHandlerRegistry();
156
+ const list = registry.handlers.get(normalizedKind) ?? [];
157
+ list.push(handler);
158
+ registry.handlers.set(normalizedKind, list);
159
+ return () => {
160
+ const updated = registry.handlers.get(normalizedKind) ?? [];
161
+ const index = updated.indexOf(handler);
162
+ if (index !== -1) {
163
+ updated.splice(index, 1);
164
+ registry.handlers.set(normalizedKind, updated);
165
+ }
166
+ };
167
+ }
168
+
169
+ export function getTelegramInboundProgrammaticHandlers(
170
+ kind: string,
171
+ ): TelegramInboundProgrammaticHandler[] {
172
+ const registry = getOrCreateInboundHandlerRegistry();
173
+ return [
174
+ ...(registry.handlers.get(kind) ?? []),
175
+ ...(kind === "*" ? [] : (registry.handlers.get("*") ?? [])),
176
+ ];
177
+ }
178
+
179
+ export function hasTelegramInboundHandler(kind?: string): boolean {
180
+ const registry = getOrCreateInboundHandlerRegistry();
181
+ if (kind !== undefined) return (registry.handlers.get(kind)?.length ?? 0) > 0;
182
+ return Array.from(registry.handlers.values()).some((list) => list.length > 0);
183
+ }
184
+
185
+ export function clearTelegramInboundHandlers(): void {
186
+ getOrCreateInboundHandlerRegistry().handlers.clear();
187
+ }
188
+
189
+ function normalizeInboundProgrammaticHandlerText(
190
+ result: TelegramInboundProgrammaticHandlerResult,
191
+ ): string | undefined {
192
+ const text = typeof result === "string" ? result : result?.text;
193
+ const normalized = text?.trim();
194
+ return normalized || undefined;
195
+ }
196
+
106
197
  function normalizeStringList(value: string | string[] | undefined): string[] {
107
198
  if (Array.isArray(value)) {
108
199
  return value
@@ -467,9 +558,84 @@ async function processTelegramTextHandlers(options: {
467
558
  });
468
559
  }
469
560
  }
561
+ for (const handler of getTelegramInboundProgrammaticHandlers("text")) {
562
+ try {
563
+ const output = normalizeInboundProgrammaticHandlerText(
564
+ await handler(
565
+ { kind: "text", text, mimeType: "text/plain" },
566
+ { cwd: options.cwd },
567
+ ),
568
+ );
569
+ if (output) text = output;
570
+ } catch (error) {
571
+ options.recordRuntimeEvent?.("inbound-programmatic-handler", error, {
572
+ kind: "text",
573
+ });
574
+ }
575
+ }
470
576
  return text;
471
577
  }
472
578
 
579
+ function isTelegramVoiceLikeFile(file: TelegramInboundHandlerFile): boolean {
580
+ return (
581
+ file.kind === "voice" ||
582
+ file.kind === "audio" ||
583
+ matchesWildcard("audio/*", file.mimeType)
584
+ );
585
+ }
586
+
587
+ async function processTelegramFileWithProgrammaticHandlers(
588
+ file: TelegramInboundHandlerFile,
589
+ options: {
590
+ cwd: string;
591
+ recordRuntimeEvent?: TelegramInboundHandlerRuntimeDeps<unknown>["recordRuntimeEvent"];
592
+ },
593
+ ): Promise<string | undefined> {
594
+ const kind = file.kind || "*";
595
+ for (const handler of getTelegramInboundProgrammaticHandlers(kind)) {
596
+ try {
597
+ const output = normalizeInboundProgrammaticHandlerText(
598
+ await handler(
599
+ {
600
+ kind,
601
+ file,
602
+ mimeType: file.mimeType,
603
+ },
604
+ { cwd: options.cwd },
605
+ ),
606
+ );
607
+ if (output) return output;
608
+ } catch (error) {
609
+ options.recordRuntimeEvent?.("inbound-programmatic-handler", error, {
610
+ fileName: file.fileName || basename(file.path),
611
+ kind,
612
+ });
613
+ }
614
+ }
615
+ return undefined;
616
+ }
617
+
618
+ async function transcribeTelegramVoiceFileWithProviders(
619
+ file: TelegramInboundHandlerFile,
620
+ options: {
621
+ recordRuntimeEvent?: TelegramInboundHandlerRuntimeDeps<unknown>["recordRuntimeEvent"];
622
+ },
623
+ ): Promise<string | undefined> {
624
+ if (!isTelegramVoiceLikeFile(file)) return undefined;
625
+ for (const provider of getTelegramVoiceTranscriptionProviders()) {
626
+ try {
627
+ const result = await provider(file, {});
628
+ const text = typeof result === "string" ? result : result?.text;
629
+ if (text?.trim()) return text.trim();
630
+ } catch (error) {
631
+ options.recordRuntimeEvent?.("voice-transcription-provider", error, {
632
+ fileName: file.fileName || basename(file.path),
633
+ });
634
+ }
635
+ }
636
+ return undefined;
637
+ }
638
+
473
639
  async function readBuiltInTelegramTextAttachment(
474
640
  file: TelegramInboundHandlerFile,
475
641
  ): Promise<string | undefined> {
@@ -564,6 +730,37 @@ export async function processTelegramInboundHandlers<
564
730
  });
565
731
  }
566
732
  }
733
+ if (!hasOutput) {
734
+ try {
735
+ const output = await processTelegramFileWithProgrammaticHandlers(file, {
736
+ cwd: options.cwd,
737
+ recordRuntimeEvent: options.recordRuntimeEvent,
738
+ });
739
+ if (output) {
740
+ outputs.push({ file, output, handler: { type: "programmatic" } });
741
+ hasOutput = true;
742
+ }
743
+ } catch (error) {
744
+ options.recordRuntimeEvent?.("inbound-programmatic-handler", error, {
745
+ fileName: file.fileName || basename(file.path),
746
+ });
747
+ }
748
+ }
749
+ if (!hasOutput) {
750
+ try {
751
+ const output = await transcribeTelegramVoiceFileWithProviders(file, {
752
+ recordRuntimeEvent: options.recordRuntimeEvent,
753
+ });
754
+ if (output) {
755
+ outputs.push({ file, output, handler: { type: "voice-provider" } });
756
+ hasOutput = true;
757
+ }
758
+ } catch (error) {
759
+ options.recordRuntimeEvent?.("voice-transcription-provider", error, {
760
+ fileName: file.fileName || basename(file.path),
761
+ });
762
+ }
763
+ }
567
764
  if (!hasOutput) {
568
765
  try {
569
766
  const output = await readBuiltInTelegramTextAttachment(file);
package/lib/media.ts CHANGED
@@ -463,6 +463,7 @@ export function collectTelegramFileInfos(
463
463
  isImage: false,
464
464
  });
465
465
  }
466
+ // Generic audio files (e.g. MP3 uploads) — can also trigger voice replies in "mirror" mode
466
467
  if (message.audio) {
467
468
  const fileName =
468
469
  message.audio.file_name ||
@@ -478,6 +479,8 @@ export function collectTelegramFileInfos(
478
479
  isImage: false,
479
480
  });
480
481
  }
482
+
483
+ // Voice messages (recorded via microphone) — primary trigger for "mirror" voice reply mode
481
484
  if (message.voice) {
482
485
  files.push({
483
486
  file_id: message.voice.file_id,