Initial implementation of Telegram AI customer support bot
Cloudflare Workers + Hono + D1 + KV + R2 stack with 4 specialized AI agents (onboarding, troubleshoot, asset, billing), OpenAI function calling with 7 tool definitions, human escalation, pending action approval workflow, feedback collection, audit logging, i18n (ko/en), and Workers AI fallback. 43 source files, 45 tests passing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
235
src/telegram.ts
Normal file
235
src/telegram.ts
Normal file
@@ -0,0 +1,235 @@
|
||||
// ============================================
|
||||
// Telegram Bot API Helpers
|
||||
// ============================================
|
||||
|
||||
export class TelegramError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly code?: number,
|
||||
public readonly description?: string
|
||||
) {
|
||||
super(message);
|
||||
this.name = 'TelegramError';
|
||||
}
|
||||
}
|
||||
|
||||
export interface InlineKeyboardButton {
|
||||
text: string;
|
||||
url?: string;
|
||||
callback_data?: string;
|
||||
web_app?: { url: string };
|
||||
}
|
||||
|
||||
async function callTelegramAPI(
|
||||
token: string,
|
||||
method: string,
|
||||
body: Record<string, unknown>
|
||||
): Promise<Response> {
|
||||
const response = await fetch(
|
||||
`https://api.telegram.org/bot${token}/${method}`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(body),
|
||||
}
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
let description = '';
|
||||
try {
|
||||
const errorData = await response.json() as { description?: string };
|
||||
description = errorData.description || '';
|
||||
} catch {
|
||||
// JSON parse failure ignored
|
||||
}
|
||||
throw new TelegramError(
|
||||
`Telegram API ${method} failed: ${response.status}`,
|
||||
response.status,
|
||||
description
|
||||
);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
function wrapTelegramCall(method: string, fn: () => Promise<Response>): Promise<void> {
|
||||
return fn().then(() => undefined).catch((error: unknown) => {
|
||||
if (error instanceof TelegramError) throw error;
|
||||
throw new TelegramError(
|
||||
`Network error in ${method}`,
|
||||
undefined,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
export async function sendMessage(
|
||||
token: string,
|
||||
chatId: number,
|
||||
text: string,
|
||||
options?: {
|
||||
parse_mode?: 'HTML' | 'Markdown' | 'MarkdownV2';
|
||||
reply_to_message_id?: number;
|
||||
disable_notification?: boolean;
|
||||
}
|
||||
): Promise<void> {
|
||||
return wrapTelegramCall('sendMessage', () =>
|
||||
callTelegramAPI(token, 'sendMessage', {
|
||||
chat_id: chatId,
|
||||
text,
|
||||
parse_mode: options?.parse_mode || 'HTML',
|
||||
reply_to_message_id: options?.reply_to_message_id,
|
||||
disable_notification: options?.disable_notification,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export async function sendMessageWithKeyboard(
|
||||
token: string,
|
||||
chatId: number,
|
||||
text: string,
|
||||
keyboard: InlineKeyboardButton[][],
|
||||
options?: {
|
||||
parse_mode?: 'HTML' | 'Markdown' | 'MarkdownV2';
|
||||
}
|
||||
): Promise<void> {
|
||||
return wrapTelegramCall('sendMessageWithKeyboard', () =>
|
||||
callTelegramAPI(token, 'sendMessage', {
|
||||
chat_id: chatId,
|
||||
text,
|
||||
parse_mode: options?.parse_mode || 'HTML',
|
||||
reply_markup: { inline_keyboard: keyboard },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export async function sendChatAction(
|
||||
token: string,
|
||||
chatId: number,
|
||||
action: 'typing' | 'upload_photo' | 'upload_document' = 'typing'
|
||||
): Promise<void> {
|
||||
return wrapTelegramCall('sendChatAction', () =>
|
||||
callTelegramAPI(token, 'sendChatAction', {
|
||||
chat_id: chatId,
|
||||
action,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export async function answerCallbackQuery(
|
||||
token: string,
|
||||
callbackQueryId: string,
|
||||
options?: {
|
||||
text?: string;
|
||||
show_alert?: boolean;
|
||||
}
|
||||
): Promise<void> {
|
||||
return wrapTelegramCall('answerCallbackQuery', () =>
|
||||
callTelegramAPI(token, 'answerCallbackQuery', {
|
||||
callback_query_id: callbackQueryId,
|
||||
text: options?.text,
|
||||
show_alert: options?.show_alert,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export async function editMessageText(
|
||||
token: string,
|
||||
chatId: number,
|
||||
messageId: number,
|
||||
text: string,
|
||||
options?: {
|
||||
parse_mode?: 'HTML' | 'Markdown' | 'MarkdownV2';
|
||||
reply_markup?: { inline_keyboard: InlineKeyboardButton[][] };
|
||||
}
|
||||
): Promise<void> {
|
||||
return wrapTelegramCall('editMessageText', () =>
|
||||
callTelegramAPI(token, 'editMessageText', {
|
||||
chat_id: chatId,
|
||||
message_id: messageId,
|
||||
text,
|
||||
parse_mode: options?.parse_mode || 'HTML',
|
||||
reply_markup: options?.reply_markup,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
export async function sendPhoto(
|
||||
token: string,
|
||||
chatId: number,
|
||||
photo: ArrayBuffer,
|
||||
options?: {
|
||||
caption?: string;
|
||||
parse_mode?: 'HTML' | 'Markdown' | 'MarkdownV2';
|
||||
reply_to_message_id?: number;
|
||||
}
|
||||
): Promise<void> {
|
||||
const formData = new FormData();
|
||||
formData.append('chat_id', String(chatId));
|
||||
formData.append('photo', new Blob([photo], { type: 'image/png' }), 'diagram.png');
|
||||
if (options?.caption) {
|
||||
formData.append('caption', options.caption);
|
||||
formData.append('parse_mode', options.parse_mode || 'HTML');
|
||||
}
|
||||
if (options?.reply_to_message_id) {
|
||||
formData.append('reply_to_message_id', String(options.reply_to_message_id));
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch(
|
||||
`https://api.telegram.org/bot${token}/sendPhoto`,
|
||||
{ method: 'POST', body: formData }
|
||||
);
|
||||
|
||||
if (!response.ok) {
|
||||
let description = '';
|
||||
try {
|
||||
const errorData = await response.json() as { description?: string };
|
||||
description = errorData.description || '';
|
||||
} catch {
|
||||
// JSON parse failure ignored
|
||||
}
|
||||
throw new TelegramError(
|
||||
`Telegram API sendPhoto failed: ${response.status}`,
|
||||
response.status,
|
||||
description
|
||||
);
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof TelegramError) throw error;
|
||||
throw new TelegramError(
|
||||
'Network error in sendPhoto',
|
||||
undefined,
|
||||
error instanceof Error ? error.message : String(error)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function setWebhook(
|
||||
token: string,
|
||||
webhookUrl: string,
|
||||
secretToken: string
|
||||
): Promise<{ ok: boolean; description?: string }> {
|
||||
const response = await fetch(
|
||||
`https://api.telegram.org/bot${token}/setWebhook`,
|
||||
{
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
url: webhookUrl,
|
||||
secret_token: secretToken,
|
||||
allowed_updates: ['message', 'callback_query'],
|
||||
drop_pending_updates: true,
|
||||
}),
|
||||
}
|
||||
);
|
||||
|
||||
return response.json() as Promise<{ ok: boolean; description?: string }>;
|
||||
}
|
||||
|
||||
export async function getWebhookInfo(token: string): Promise<unknown> {
|
||||
const response = await fetch(
|
||||
`https://api.telegram.org/bot${token}/getWebhookInfo`
|
||||
);
|
||||
return response.json();
|
||||
}
|
||||
Reference in New Issue
Block a user