Telegram: enable grammY throttler and webhook tests
parent
4d3d9cca2a
commit
5f5846a08b
|
|
@ -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.
|
||||
|
||||
### Docs
|
||||
- Added `docs/telegram.md` outlining the upcoming Telegram Bot API provider (grammY-based) and how it will share the `main` session.
|
||||
- CLI now exposes `relay:telegram` and text/media sends via `--provider telegram`; typing/webhook still pending.
|
||||
- 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 exposes `relay:telegram` (grammY) and text/media sends via `--provider telegram`; webhook/proxy options documented.
|
||||
|
||||
## 1.5.0 — 2025-12-05
|
||||
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ Create `~/.clawdis/clawdis.json`:
|
|||
- [Security](./docs/security.md)
|
||||
- [Troubleshooting](./docs/troubleshooting.md)
|
||||
- [The Lore](./docs/lore.md) 🦞
|
||||
- [Telegram (Bot API) — WIP](./docs/telegram.md)
|
||||
- [Telegram (Bot API)](./docs/telegram.md)
|
||||
|
||||
## Clawd
|
||||
|
||||
|
|
@ -120,8 +120,8 @@ clawdis login # Scan QR code
|
|||
clawdis relay # Start listening
|
||||
```
|
||||
|
||||
### Telegram (Bot API) — WIP
|
||||
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.
|
||||
### Telegram (Bot API)
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
## 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.
|
||||
- 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.
|
||||
|
|
|
|||
|
|
@ -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 { Bot, InputFile, webhookCallback } from "grammy";
|
||||
import { apiThrottler } from "@grammyjs/transformer-throttler";
|
||||
import type { ApiClientOptions } from "grammy";
|
||||
|
||||
import { chunkText } from "../auto-reply/chunk.js";
|
||||
|
|
@ -38,6 +39,7 @@ export function createTelegramBot(opts: TelegramBotOptions) {
|
|||
: undefined;
|
||||
|
||||
const bot = new Bot(opts.token, { client });
|
||||
bot.api.config.use(apiThrottler());
|
||||
|
||||
const cfg = loadConfig();
|
||||
const requireMention =
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue