abstract-bot-api is a small abstraction layer over chat providers.
It solves two practical problems:
- You want one handler style across multiple bot providers.
- You want deeply nested code to be able to reply, show typing, send progress, or inspect the current incoming event without manually threading provider clients through your call stack.
The second part is implemented with request-scoped dependency injection via
@uri/inject.
import * as botApi from "jsr:@uri/abstract-bot-api";Current provider support:
- Telegram
- Slack
- Microsoft Teams
- WhatsApp Cloud API
- Facebook Messenger
- Green API
Inside a provider webhook handler, the library injects a request-scoped context. From anywhere further down the stack, your code can call the abstract API directly.
Common examples:
import {
lastEvent,
medium,
messageId,
progressBar,
reply,
sendQuotedReply,
sendReaction,
typing,
userId,
withSpinner,
withTyping,
} from "jsr:@uri/abstract-bot-api";Useful runtime accessors:
lastEvent(): ConversationEventmedium(): stringuserId(): stringmessageId(): stringreferenceId(): stringbotPhone(): string
Useful actions:
reply(text): Promise<string>sendQuotedReply(text, replyToMessageId): Promise<string>editMessage(messageId, text): Promise<void>deleteMessage(chatId, messageId): Promise<void>replyImage(payload): Promise<string>sendFile(url): Promise<void>typing(): Promise<void>sendReaction(messageId, emoji): Promise<void>
Useful helpers:
withTyping(fn)withSpinner(text, fn)progressBar(text): Promise<(percentage: number) => Promise<void>>
Incoming events are normalized into a ConversationEvent:
type ConversationEvent =
| {
kind: "message";
text?: string;
contact?: { phone: string; name: string };
attachments?: MediaAttachment[];
ownPhone?: string;
referencedMessageId?: string;
}
| {
kind: "edit";
text: string;
onMessageId: string;
attachments?: MediaAttachment[];
}
| {
kind: "reaction";
reaction: string;
onMessageId: string;
};That means your task handler can usually ignore provider-specific webhook shapes
and just inspect lastEvent().
import { coerce, sleep } from "gamla";
import {
bouncerServer,
lastEvent,
makeTelegramHandler,
reply,
setTelegramWebhook,
whatsappBusinessHandler,
whatsappWebhookVerificationHandler,
withSpinner,
} from "jsr:@uri/abstract-bot-api";
const url = coerce(Deno.env.get("URL"));
const port = coerce(Deno.env.get("PORT"));
const telegramToken = coerce(Deno.env.get("TELEGRAM_TOKEN"));
const telegramWebhookSecret = coerce(
Deno.env.get("TELEGRAM_WEBHOOK_SECRET"),
);
const telegramPath = "/telegram";
const whatsappAccessToken = coerce(Deno.env.get("WHATSAPP_ACCESS_TOKEN"));
const whatsappAppSecret = coerce(Deno.env.get("WHATSAPP_APP_SECRET"));
const whatsappVerificationToken = coerce(
Deno.env.get("WHATSAPP_VERIFICATION_TOKEN"),
);
const whatsappPath = "/whatsapp";
const handleMessage = async () => {
const event = lastEvent();
await withSpinner("Thinking", sleep)(1000);
return reply(`Got ${JSON.stringify(event)}`);
};
await bouncerServer(url, port, [
makeTelegramHandler(
telegramToken,
telegramPath,
handleMessage,
telegramWebhookSecret,
),
whatsappBusinessHandler(
whatsappAccessToken,
whatsappAppSecret,
whatsappPath,
handleMessage,
),
whatsappWebhookVerificationHandler(
whatsappVerificationToken,
whatsappPath,
),
]);
await setTelegramWebhook(
telegramToken,
`${url}${telegramPath}`,
telegramWebhookSecret,
);Webhook verification is enforced before bounced handlers are queued.
This is important because many handlers use bounce: true, which means inbound
requests are accepted quickly and processed asynchronously. Verification
therefore must happen at the HTTP boundary, not later inside deferred execution.
Current verification model by provider:
| Provider | Verification mechanism |
|---|---|
| Telegram | X-Telegram-Bot-Api-Secret-Token |
| Slack | X-Slack-Signature + X-Slack-Request-Timestamp |
| WhatsApp Cloud API | X-Hub-Signature-256 |
| Facebook Messenger | X-Hub-Signature-256 |
| Microsoft Teams | Bot Framework JWT in Authorization |
| Green API | configured Authorization header token |
The internal deferred endpoint is also protected and cannot be called directly from outside the process.
Handler:
makeTelegramHandler(
telegramToken,
path,
doTask,
secretToken,
);Webhook registration:
await setTelegramWebhook(telegramToken, webhookUrl, secretToken);Requirements:
- Set a webhook secret token when registering the webhook.
- Pass the same secret token to
makeTelegramHandler.
Handler:
slackWebhookHandler(
botToken,
signingSecret,
path,
doTask,
);Requirements:
- Use your Slack bot token for outbound API calls.
- Use your Slack signing secret for inbound request verification.
Notes:
- Supports Slack
url_verification. - Normalizes message, edit, and reaction events.
Handler:
teamsWebhookHandler(
appId,
appPassword,
path,
doTask,
);Requirements:
- Use Bot Framework app credentials for outbound calls.
- Inbound requests are verified via Bot Framework JWT validation.
Handlers:
whatsappBusinessHandler(
accessToken,
appSecret,
path,
doTask,
);
whatsappWebhookVerificationHandler(
verificationToken,
path,
);Requirements:
accessTokenfor outbound Graph API calls.appSecretforX-Hub-Signature-256verification.verificationTokenfor Meta webhook subscription challenge.
Handlers:
messengerWebhookHandler(
accessToken,
appSecret,
path,
doTask,
);
messengerWebhookVerificationHandler(
verificationToken,
path,
);Requirements:
accessTokenfor outbound Messenger API calls.appSecretforX-Hub-Signature-256verification.verificationTokenfor Meta webhook subscription challenge.
Register webhook:
await registerWebhook(credentials, webhookUrl, webhookAuthorizationHeader);Handler:
greenApiHandler(
credentials,
path,
doTask,
webhookAuthorizationHeader,
);Requirements:
- Configure
webhookAuthorizationHeaderwhen registering the webhook. - Pass the same expected header value to
greenApiHandler.
Notes:
- Green API appears to document webhook auth via
Authorizationheader token rather than signed request payloads.
bouncerServer(domain, port, endpoints) runs a small HTTP server and routes
inbound requests to endpoint handlers.
Endpoint types:
bounce: falsebounce: true
bounce: true means the request is authenticated, acknowledged immediately, and
processed asynchronously through the library’s deferred internal endpoint.
That deferred endpoint is protected internally by a generated token.
Recent security changes introduced signature or token verification into the inbound handlers. That means these APIs now require more explicit secrets than older versions:
makeTelegramHandlerrequiressecretTokensetTelegramWebhookacceptssecretTokenwhatsappBusinessHandlerrequiresappSecretmessengerWebhookHandlerrequiresappSecretslackWebhookHandlerrequiressigningSecretgreenApiHandlerrequireswebhookAuthorizationHeaderregisterWebhookacceptswebhookAuthorizationHeader
This library intentionally keeps the abstraction small.
It does not try to flatten every provider feature into one giant interface. Instead, it focuses on the operations that are commonly needed in real bot handlers:
- read the current inbound event
- reply
- reply in-thread or quoted
- show typing/progress/spinner state
- edit or delete messages when supported
- access context without passing provider clients everywhere
Run checks:
deno task checkRun focused tests:
deno test --allow-env --allow-net src/taskBouncer.test.ts src/webhookAuth.test.ts