Skip to content

Commit 9a9e4fb

Browse files
committed
feat: enhance TelegramChatSurfaceAdapter with audio handling and command parsing improvements
1 parent d404e02 commit 9a9e4fb

3 files changed

Lines changed: 171 additions & 586 deletions

File tree

index.ts

Lines changed: 171 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import {
22
type ChatSurfaceAdapter,
3+
type ChatSurfaceEvent,
34
type ChatSurfaceEventSink,
45
type ChatSurfaceIncomingMessage,
56
type ChatSurfaceRequestContext,
@@ -24,6 +25,16 @@ export {
2425
type TelegramUpdate = {
2526
message?: {
2627
text?: string;
28+
caption?: string;
29+
voice?: {
30+
file_id?: string;
31+
mime_type?: string;
32+
};
33+
audio?: {
34+
file_id?: string;
35+
file_name?: string;
36+
mime_type?: string;
37+
};
2738
chat?: {
2839
id?: number | string;
2940
};
@@ -37,12 +48,29 @@ type TelegramUpdate = {
3748
};
3849
};
3950

51+
type TelegramGetFileResponse = {
52+
ok: boolean;
53+
result?: {
54+
file_path?: string;
55+
};
56+
description?: string;
57+
};
58+
4059
type ChatSurfaceConnectAction = {
4160
type: "url";
4261
label: string;
4362
url: string;
4463
};
4564

65+
type TelegramChatSurfaceEvent =
66+
| ChatSurfaceEvent
67+
| {
68+
type: "audio";
69+
audio: Buffer;
70+
filename: string;
71+
mimeType: string;
72+
};
73+
4674
const TELEGRAM_API_BASE_URL = "https://api.telegram.org";
4775
const TELEGRAM_SECRET_HEADER = "x-telegram-bot-api-secret-token";
4876
const TELEGRAM_MESSAGE_MAX_LENGTH = 4096;
@@ -93,14 +121,16 @@ function splitTelegramMessage(text: string) {
93121
return chunks;
94122
}
95123

96-
function parseTelegramStartPayload(text: string) {
124+
function parseTelegramStartCommand(text: string) {
97125
const [command, ...payloadParts] = text.trim().split(TELEGRAM_COMMAND_PARTS_RE);
126+
const isStartCommand =
127+
command === TELEGRAM_START_COMMAND_PREFIX ||
128+
command.startsWith(`${TELEGRAM_START_COMMAND_PREFIX}@`);
98129

99-
if (command !== TELEGRAM_START_COMMAND_PREFIX && !command.startsWith(`${TELEGRAM_START_COMMAND_PREFIX}@`)) {
100-
return null;
101-
}
102-
103-
return payloadParts.join(" ") || null;
130+
return {
131+
isStartCommand,
132+
payload: isStartCommand ? payloadParts.join(" ") || null : null,
133+
};
104134
}
105135

106136
export class TelegramChatSurfaceAdapter implements ChatSurfaceAdapter {
@@ -132,24 +162,59 @@ export class TelegramChatSurfaceAdapter implements ChatSurfaceAdapter {
132162
}
133163

134164
const update = ctx.body as TelegramUpdate;
135-
const text = update.message?.text;
136-
const chatId = update.message?.chat?.id;
137-
const userId = update.message?.from?.id;
165+
const message = update.message;
166+
const text = message?.text;
167+
const chatId = message?.chat?.id;
168+
const userId = message?.from?.id;
138169

139-
if (!text || chatId === undefined || userId === undefined) {
170+
if (chatId === undefined || userId === undefined) {
140171
return null;
141172
}
142173

143-
const startPayload = parseTelegramStartPayload(text);
174+
const startCommand = text
175+
? parseTelegramStartCommand(text)
176+
: { isStartCommand: false, payload: null };
177+
178+
if (text) {
179+
return {
180+
surface: this.name,
181+
prompt: text,
182+
externalConversationId: String(chatId),
183+
externalUserId: String(userId),
184+
userTimeZone: "UTC",
185+
metadata: {
186+
isStartCommand: startCommand.isStartCommand,
187+
startPayload: startCommand.payload,
188+
telegramUpdate: update,
189+
},
190+
};
191+
}
192+
193+
const voiceFileId = message?.voice?.file_id;
194+
const audioFileId = message?.audio?.file_id;
195+
const fileId = voiceFileId ?? audioFileId;
196+
197+
if (!fileId) {
198+
return null;
199+
}
200+
201+
const audio = await this.downloadTelegramFile({
202+
fileId,
203+
filename: message?.audio?.file_name ?? (voiceFileId ? "telegram-voice.ogg" : "telegram-audio"),
204+
mimeType: message?.voice?.mime_type ?? message?.audio?.mime_type ?? "application/octet-stream",
205+
abortSignal: ctx.abortSignal,
206+
});
144207

145208
return {
146209
surface: this.name,
147-
prompt: text,
210+
prompt: message?.caption ?? "",
211+
audio,
148212
externalConversationId: String(chatId),
149213
externalUserId: String(userId),
150214
userTimeZone: "UTC",
151215
metadata: {
152-
startPayload,
216+
isStartCommand: startCommand.isStartCommand,
217+
startPayload: startCommand.payload,
153218
telegramUpdate: update,
154219
},
155220
};
@@ -245,7 +310,7 @@ export class TelegramChatSurfaceAdapter implements ChatSurfaceAdapter {
245310
startTyping();
246311

247312
return {
248-
emit: async (event) => {
313+
emit: async (event: TelegramChatSurfaceEvent) => {
249314
if (closed) {
250315
return;
251316
}
@@ -265,9 +330,16 @@ export class TelegramChatSurfaceAdapter implements ChatSurfaceAdapter {
265330
stopTyping();
266331
clearDraftTimer();
267332

268-
await this.sendFinalMessage(
333+
await this.sendFinalMessage(chatId, text || event.text);
334+
return;
335+
}
336+
337+
if (event.type === "audio") {
338+
await this.sendAudioFile(
269339
chatId,
270-
text || event.text,
340+
event.audio,
341+
event.filename,
342+
event.mimeType,
271343
);
272344
return;
273345
}
@@ -298,21 +370,11 @@ export class TelegramChatSurfaceAdapter implements ChatSurfaceAdapter {
298370
}
299371

300372
for (const chunk of splitTelegramMessage(text)) {
301-
const response = await fetch(`${TELEGRAM_API_BASE_URL}/bot${this.options.botToken}/sendMessage`, {
302-
method: "POST",
303-
headers: {
304-
"Content-Type": "application/json",
305-
},
306-
body: JSON.stringify({
307-
chat_id: chatId,
308-
text: chunk,
309-
parse_mode: "Markdown",
310-
}),
373+
await this.telegramJson("sendMessage", {
374+
chat_id: chatId,
375+
text: chunk,
376+
parse_mode: "Markdown",
311377
});
312-
313-
if (!response.ok) {
314-
throw new Error(`Telegram sendMessage failed: ${response.status} ${await response.text()}`);
315-
}
316378
}
317379
}
318380

@@ -334,30 +396,95 @@ export class TelegramChatSurfaceAdapter implements ChatSurfaceAdapter {
334396
type: "image/png",
335397
}), filename);
336398

337-
const response = await fetch(`${TELEGRAM_API_BASE_URL}/bot${this.options.botToken}/sendPhoto`, {
399+
await this.telegramMultipart("sendPhoto", formData);
400+
}
401+
402+
private async sendChatAction(chatId: string, action: "typing" | "upload_voice" | "upload_audio") {
403+
await this.telegramJson("sendChatAction", {
404+
chat_id: chatId,
405+
action,
406+
});
407+
}
408+
409+
private async downloadTelegramFile(input: {
410+
fileId: string;
411+
filename: string;
412+
mimeType: string;
413+
abortSignal: AbortSignal;
414+
}) {
415+
const fileResponse = await fetch(
416+
`${TELEGRAM_API_BASE_URL}/bot${this.options.botToken}/getFile?file_id=${encodeURIComponent(input.fileId)}`,
417+
{ signal: input.abortSignal },
418+
);
419+
420+
if (!fileResponse.ok) {
421+
throw new Error(`Telegram getFile failed: ${fileResponse.status} ${await fileResponse.text()}`);
422+
}
423+
424+
const fileData = await fileResponse.json() as TelegramGetFileResponse;
425+
const filePath = fileData.result?.file_path;
426+
427+
if (!fileData.ok || !filePath) {
428+
throw new Error(`Telegram getFile failed: ${fileData.description ?? "file_path is missing"}`);
429+
}
430+
431+
const downloadResponse = await fetch(
432+
`${TELEGRAM_API_BASE_URL}/file/bot${this.options.botToken}/${filePath}`,
433+
{ signal: input.abortSignal },
434+
);
435+
436+
if (!downloadResponse.ok) {
437+
throw new Error(`Telegram file download failed: ${downloadResponse.status} ${await downloadResponse.text()}`);
438+
}
439+
440+
return {
441+
buffer: Buffer.from(await downloadResponse.arrayBuffer()),
442+
filename: input.filename,
443+
mimeType: input.mimeType,
444+
};
445+
}
446+
447+
private async sendAudioFile(
448+
chatId: string,
449+
audio: Buffer,
450+
filename: string,
451+
mimeType: string,
452+
) {
453+
const sendAsVoice = ["audio/ogg", "audio/opus"].includes(mimeType) || filename.endsWith(".ogg") || filename.endsWith(".opus");
454+
const method = sendAsVoice ? "sendVoice" : "sendAudio";
455+
const fieldName = sendAsVoice ? "voice" : "audio";
456+
const audioBytes = new Uint8Array(audio);
457+
const formData = new FormData();
458+
formData.append("chat_id", chatId);
459+
formData.append(fieldName, new Blob([audioBytes], {
460+
type: mimeType,
461+
}), filename);
462+
463+
await this.telegramMultipart(method, formData);
464+
}
465+
466+
private async telegramMultipart(method: string, formData: FormData) {
467+
const response = await fetch(`${TELEGRAM_API_BASE_URL}/bot${this.options.botToken}/${method}`, {
338468
method: "POST",
339469
body: formData,
340470
});
341471

342472
if (!response.ok) {
343-
throw new Error(`Telegram sendPhoto failed: ${response.status} ${await response.text()}`);
473+
throw new Error(`Telegram ${method} failed: ${response.status} ${await response.text()}`);
344474
}
345475
}
346476

347-
private async sendChatAction(chatId: string, action: "typing") {
348-
const response = await fetch(`${TELEGRAM_API_BASE_URL}/bot${this.options.botToken}/sendChatAction`, {
477+
private async telegramJson(method: string, body: unknown) {
478+
const response = await fetch(`${TELEGRAM_API_BASE_URL}/bot${this.options.botToken}/${method}`, {
349479
method: "POST",
350480
headers: {
351481
"Content-Type": "application/json",
352482
},
353-
body: JSON.stringify({
354-
chat_id: chatId,
355-
action,
356-
}),
483+
body: JSON.stringify(body),
357484
});
358485

359486
if (!response.ok) {
360-
throw new Error(`Telegram sendChatAction failed: ${response.status} ${await response.text()}`);
487+
throw new Error(`Telegram ${method} failed: ${response.status} ${await response.text()}`);
361488
}
362489
}
363490

@@ -367,22 +494,12 @@ export class TelegramChatSurfaceAdapter implements ChatSurfaceAdapter {
367494
text: string;
368495
parseMode?: "Markdown" | "MarkdownV2" | "HTML";
369496
}) {
370-
const response = await fetch(`${TELEGRAM_API_BASE_URL}/bot${this.options.botToken}/sendMessageDraft`, {
371-
method: "POST",
372-
headers: {
373-
"Content-Type": "application/json",
374-
},
375-
body: JSON.stringify({
376-
chat_id: Number(input.chatId),
377-
draft_id: input.draftId,
378-
text: input.text,
379-
parse_mode: input.parseMode,
380-
}),
497+
await this.telegramJson("sendMessageDraft", {
498+
chat_id: Number(input.chatId),
499+
draft_id: input.draftId,
500+
text: input.text,
501+
parse_mode: input.parseMode,
381502
});
382-
383-
if (!response.ok) {
384-
throw new Error(`Telegram sendMessageDraft failed: ${response.status} ${await response.text()}`);
385-
}
386503
}
387504
}
388505

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@
3333
"playwright": "^1.57.0"
3434
},
3535
"peerDependencies": {
36-
"@adminforth/agent": ">=1.0.2",
3736
"adminforth": ">=2.60.0"
3837
},
3938
"release": {

0 commit comments

Comments
 (0)