initial: webhook telegram adapter for pi with streaming replies

- webhook server with secret validation, rate limiting, body guards
- streaming replies via sendMessage + editMessageText throttled loop
- RPC session management for persistent conversations
- 15/15 tests passing
This commit is contained in:
Harivansh Rathi 2026-04-03 05:30:05 +00:00
parent 809e9b1df5
commit ce9abc2a8e
18 changed files with 6991 additions and 1 deletions

97
src/telegram-api.ts Normal file
View file

@ -0,0 +1,97 @@
/**
* pi-telegram-webhook Thin Telegram Bot API wrapper.
*/
const BASE_URL = "https://api.telegram.org/bot";
export class TelegramAPI {
private token: string;
constructor(token: string) {
this.token = token;
}
private url(method: string): string {
return `${BASE_URL}${this.token}/${method}`;
}
private async request(method: string, body?: Record<string, unknown>): Promise<unknown> {
const res = await fetch(this.url(method), {
method: "POST",
headers: { "Content-Type": "application/json" },
body: body ? JSON.stringify(body) : undefined,
});
if (!res.ok) {
const text = await res.text().catch(() => "unknown");
throw new Error(`Telegram API ${method} failed (${res.status}): ${text}`);
}
const data = await res.json();
if (!(data as { ok: boolean }).ok) {
throw new Error(`Telegram API ${method} failed: ${JSON.stringify(data)}`);
}
return (data as { result: unknown }).result;
}
async getMe(): Promise<unknown> {
return this.request("getMe");
}
async sendMessage(
chatId: string | number,
text: string,
options?: { parse_mode?: string; reply_to_message_id?: number }
): Promise<{ message_id: number }> {
const body: Record<string, unknown> = { chat_id: chatId, text };
if (options?.parse_mode) body.parse_mode = options.parse_mode;
if (options?.reply_to_message_id) body.reply_to_message_id = options.reply_to_message_id;
return (await this.request("sendMessage", body)) as { message_id: number };
}
async editMessageText(
chatId: string | number,
messageId: number,
text: string,
options?: { parse_mode?: string }
): Promise<void> {
const body: Record<string, unknown> = {
chat_id: chatId,
message_id: messageId,
text,
};
if (options?.parse_mode) body.parse_mode = options.parse_mode;
await this.request("editMessageText", body);
}
async deleteMessage(chatId: string | number, messageId: number): Promise<void> {
await this.request("deleteMessage", {
chat_id: chatId,
message_id: messageId,
});
}
async sendChatAction(chatId: string | number, action = "typing"): Promise<void> {
await this.request("sendChatAction", {
chat_id: chatId,
action,
}).catch(() => {
// Best-effort
});
}
async setWebhook(url: string, secret: string): Promise<void> {
await this.request("setWebhook", {
url,
secret_token: secret,
allowed_updates: ["message"],
});
}
async deleteWebhook(): Promise<void> {
await this.request("deleteWebhook", { drop_pending_updates: false });
}
}