Telegram: enable grammY throttler and webhook tests
This commit is contained in:
@@ -34,8 +34,8 @@ First Clawdis release after the Warelay rebrand. This is a semver-major because
|
|||||||
- Launchd PATH and helper lookup hardened for packaged macOS builds; health probes surface missing binaries quickly.
|
- Launchd PATH and helper lookup hardened for packaged macOS builds; health probes surface missing binaries quickly.
|
||||||
|
|
||||||
### Docs
|
### Docs
|
||||||
- Added `docs/telegram.md` outlining the upcoming Telegram Bot API provider (grammY-based) and how it will share the `main` session.
|
- Added `docs/telegram.md` outlining the Telegram Bot API provider (grammY) and how it shares the `main` session. Default grammY throttler keeps Bot API calls under rate limits.
|
||||||
- CLI now exposes `relay:telegram` and text/media sends via `--provider telegram`; typing/webhook still pending.
|
- CLI exposes `relay:telegram` (grammY) and text/media sends via `--provider telegram`; webhook/proxy options documented.
|
||||||
|
|
||||||
## 1.5.0 — 2025-12-05
|
## 1.5.0 — 2025-12-05
|
||||||
|
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ Create `~/.clawdis/clawdis.json`:
|
|||||||
- [Security](./docs/security.md)
|
- [Security](./docs/security.md)
|
||||||
- [Troubleshooting](./docs/troubleshooting.md)
|
- [Troubleshooting](./docs/troubleshooting.md)
|
||||||
- [The Lore](./docs/lore.md) 🦞
|
- [The Lore](./docs/lore.md) 🦞
|
||||||
- [Telegram (Bot API) — WIP](./docs/telegram.md)
|
- [Telegram (Bot API)](./docs/telegram.md)
|
||||||
|
|
||||||
## Clawd
|
## Clawd
|
||||||
|
|
||||||
@@ -120,8 +120,8 @@ clawdis login # Scan QR code
|
|||||||
clawdis relay # Start listening
|
clawdis relay # Start listening
|
||||||
```
|
```
|
||||||
|
|
||||||
### Telegram (Bot API) — WIP
|
### Telegram (Bot API)
|
||||||
Bot-mode support (long-poll) shares the same `main` session as WhatsApp/WebChat, with groups kept isolated. Text and media send work via `clawdis send --provider telegram`; a relay is available via `clawdis relay:telegram` (TELEGRAM_BOT_TOKEN or telegram.botToken in config). See `docs/telegram.md` for current limits and setup.
|
Bot-mode support (grammY only) shares the same `main` session as WhatsApp/WebChat, with groups kept isolated. Text and media send work via `clawdis send --provider telegram`; a relay is available via `clawdis relay:telegram` (TELEGRAM_BOT_TOKEN or telegram.botToken in config). Webhook mode: `--webhook --port … --webhook-secret … --webhook-url …` (or register via BotFather). See `docs/telegram.md` for setup and limits.
|
||||||
|
|
||||||
## Commands
|
## Commands
|
||||||
|
|
||||||
|
|||||||
22
docs/research/grammy.md
Normal file
22
docs/research/grammy.md
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# grammY Integration (Telegram Bot API)
|
||||||
|
|
||||||
|
Updated: 2025-12-07
|
||||||
|
|
||||||
|
# Why grammY
|
||||||
|
- TS-first Bot API client with built-in long-poll + webhook runners, middleware, error handling, rate limiter.
|
||||||
|
- Cleaner media helpers than hand-rolling fetch + FormData; supports all Bot API methods.
|
||||||
|
- Extensible: proxy support via custom fetch, session middleware (optional), type-safe context.
|
||||||
|
|
||||||
|
# What we shipped
|
||||||
|
- **Single client path:** fetch-based implementation removed; grammY is now the sole Telegram client (send + relay) with the grammY throttler enabled by default.
|
||||||
|
- **Relay:** `monitorTelegramProvider` builds a grammY `Bot`, wires mention/allowlist gating, media download via `getFile`/`download`, and delivers replies with `sendMessage/sendPhoto/sendVideo/sendAudio/sendDocument`. Supports long-poll or webhook via `webhookCallback`.
|
||||||
|
- **Proxy:** optional `telegram.proxy` uses `undici.ProxyAgent` through grammY’s `client.baseFetch`.
|
||||||
|
- **Webhook helpers:** `webhook-set.ts` wraps `setWebhook/deleteWebhook`; `webhook.ts` hosts the callback with health + graceful shutdown and optional `--webhook-url` override.
|
||||||
|
- **Sessions:** direct chats map to `main`; groups map to `group:<chatId>`; replies route back to the same surface.
|
||||||
|
- **Config knobs:** `telegram.botToken`, `requireMention`, `allowFrom`, `mediaMaxMb`, `proxy`, `webhookSecret`, `webhookUrl`.
|
||||||
|
- **Tests:** grammy mocks cover DM + group mention gating and outbound send; more media/webhook fixtures still welcome.
|
||||||
|
|
||||||
|
Open questions
|
||||||
|
- Optional grammY plugins (throttler) if we hit Bot API 429s.
|
||||||
|
- Add more structured media tests (stickers, voice notes).
|
||||||
|
- Expose a `--public-url` flag in CLI for webhook registration convenience (currently `--webhook-url`).
|
||||||
@@ -24,7 +24,7 @@ Status: ready for bot-mode use with grammY (long-poll + webhook). Text + media s
|
|||||||
- Typing indicators (`sendChatAction`) supported; inline reply/threading supported where Telegram allows.
|
- Typing indicators (`sendChatAction`) supported; inline reply/threading supported where Telegram allows.
|
||||||
|
|
||||||
## Planned implementation details
|
## Planned implementation details
|
||||||
- Library: grammY is the only client for send + relay (fetch fallback removed).
|
- Library: grammY is the only client for send + relay (fetch fallback removed); grammY throttler is enabled by default to stay under Bot API limits.
|
||||||
- Inbound normalization: maps Bot API updates to `MsgContext` with `Surface: "telegram"`, `ChatType: direct|group`, `SenderName`, `MediaPath`/`MediaType` when attachments arrive, and `Timestamp`; groups require @bot mention by default.
|
- Inbound normalization: maps Bot API updates to `MsgContext` with `Surface: "telegram"`, `ChatType: direct|group`, `SenderName`, `MediaPath`/`MediaType` when attachments arrive, and `Timestamp`; groups require @bot mention by default.
|
||||||
- Outbound: text and media (photo/video/audio/document) with optional caption; chunked to limits. Typing cue sent best-effort.
|
- Outbound: text and media (photo/video/audio/document) with optional caption; chunked to limits. Typing cue sent best-effort.
|
||||||
- Config: `TELEGRAM_BOT_TOKEN` env or `telegram.botToken` required; `telegram.requireMention`, `telegram.allowFrom`, `telegram.mediaMaxMb`, `telegram.proxy`, `telegram.webhookSecret`, `telegram.webhookUrl` supported.
|
- Config: `TELEGRAM_BOT_TOKEN` env or `telegram.botToken` required; `telegram.requireMention`, `telegram.allowFrom`, `telegram.mediaMaxMb`, `telegram.proxy`, `telegram.webhookSecret`, `telegram.webhookUrl` supported.
|
||||||
|
|||||||
33
src/telegram/bot.test.ts
Normal file
33
src/telegram/bot.test.ts
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
const useSpy = vi.fn();
|
||||||
|
const onSpy = vi.fn();
|
||||||
|
const stopSpy = vi.fn();
|
||||||
|
const apiStub = { config: { use: useSpy } };
|
||||||
|
|
||||||
|
vi.mock("grammy", () => ({
|
||||||
|
Bot: class {
|
||||||
|
api = apiStub as any;
|
||||||
|
on = onSpy;
|
||||||
|
stop = stopSpy;
|
||||||
|
constructor(public token: string) {}
|
||||||
|
},
|
||||||
|
InputFile: class {},
|
||||||
|
webhookCallback: vi.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const throttlerSpy = vi.fn(() => "throttler");
|
||||||
|
|
||||||
|
vi.mock("@grammyjs/transformer-throttler", () => ({
|
||||||
|
apiThrottler: () => throttlerSpy(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
import { createTelegramBot } from "./bot.js";
|
||||||
|
|
||||||
|
describe("createTelegramBot", () => {
|
||||||
|
it("installs grammY throttler", () => {
|
||||||
|
createTelegramBot({ token: "tok" });
|
||||||
|
expect(throttlerSpy).toHaveBeenCalledTimes(1);
|
||||||
|
expect(useSpy).toHaveBeenCalledWith("throttler");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import { Buffer } from "node:buffer";
|
import { Buffer } from "node:buffer";
|
||||||
|
|
||||||
import { Bot, InputFile, webhookCallback } from "grammy";
|
import { Bot, InputFile, webhookCallback } from "grammy";
|
||||||
|
import { apiThrottler } from "@grammyjs/transformer-throttler";
|
||||||
import type { ApiClientOptions } from "grammy";
|
import type { ApiClientOptions } from "grammy";
|
||||||
|
|
||||||
import { chunkText } from "../auto-reply/chunk.js";
|
import { chunkText } from "../auto-reply/chunk.js";
|
||||||
@@ -38,6 +39,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const bot = new Bot(opts.token, { client });
|
const bot = new Bot(opts.token, { client });
|
||||||
|
bot.api.config.use(apiThrottler());
|
||||||
|
|
||||||
const cfg = loadConfig();
|
const cfg = loadConfig();
|
||||||
const requireMention =
|
const requireMention =
|
||||||
|
|||||||
57
src/telegram/webhook.test.ts
Normal file
57
src/telegram/webhook.test.ts
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
import { describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
import { startTelegramWebhook } from "./webhook.js";
|
||||||
|
|
||||||
|
const handlerSpy = vi.fn((req: any, res: any) => {
|
||||||
|
res.writeHead(200);
|
||||||
|
res.end("ok");
|
||||||
|
});
|
||||||
|
const setWebhookSpy = vi.fn();
|
||||||
|
const stopSpy = vi.fn();
|
||||||
|
|
||||||
|
vi.mock("grammy", () => ({
|
||||||
|
webhookCallback: () => handlerSpy,
|
||||||
|
}));
|
||||||
|
|
||||||
|
vi.mock("./bot.js", () => ({
|
||||||
|
createTelegramBot: () => ({
|
||||||
|
api: { setWebhook: setWebhookSpy },
|
||||||
|
stop: stopSpy,
|
||||||
|
}),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe("startTelegramWebhook", () => {
|
||||||
|
it("starts server, registers webhook, and serves health", async () => {
|
||||||
|
const abort = new AbortController();
|
||||||
|
const { server } = await startTelegramWebhook({
|
||||||
|
token: "tok",
|
||||||
|
port: 0, // random free port
|
||||||
|
abortSignal: abort.signal,
|
||||||
|
});
|
||||||
|
const address = server.address();
|
||||||
|
if (!address || typeof address === "string") throw new Error("no address");
|
||||||
|
const url = `http://127.0.0.1:${address.port}`;
|
||||||
|
|
||||||
|
const health = await fetch(`${url}/healthz`);
|
||||||
|
expect(health.status).toBe(200);
|
||||||
|
expect(setWebhookSpy).toHaveBeenCalled();
|
||||||
|
|
||||||
|
abort.abort();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("invokes webhook handler on matching path", async () => {
|
||||||
|
handlerSpy.mockClear();
|
||||||
|
const abort = new AbortController();
|
||||||
|
const { server } = await startTelegramWebhook({
|
||||||
|
token: "tok",
|
||||||
|
port: 0,
|
||||||
|
abortSignal: abort.signal,
|
||||||
|
path: "/hook",
|
||||||
|
});
|
||||||
|
const addr = server.address();
|
||||||
|
if (!addr || typeof addr === "string") throw new Error("no addr");
|
||||||
|
await fetch(`http://127.0.0.1:${addr.port}/hook`, { method: "POST" });
|
||||||
|
expect(handlerSpy).toHaveBeenCalled();
|
||||||
|
abort.abort();
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user