sdk.bot — Bot SDK
Build inline bots with styled buttons, callback handlers, and inline query responses using the Bot SDK.
Quick Example
Register an inline query handler that returns results with colored buttons:
import type { PluginSDK, SimpleToolDef, PluginManifest } from "@teleton-agent/sdk";
export const manifest: PluginManifest = {
name: "my-inline-bot",
version: "1.0.0",
description: "Demo inline bot with styled buttons",
bot: { inline: true, callbacks: true },
};
export const start = async (sdk: PluginSDK) => {
if (!sdk.bot) return;
sdk.bot.onInlineQuery(async (ctx) => {
return [{
id: "1",
type: "article",
title: "Hello World",
description: "Send a greeting",
content: { text: "Hello from the inline bot!" },
keyboard: [[
{ text: "Like", callback: "like:1", style: "success" },
{ text: "Dislike", callback: "dislike:1", style: "danger" },
]],
}];
});
sdk.bot.onCallback("like:*", async (ctx) => {
await ctx.answer("Thanks for the like!");
});
};Prerequisites
The Bot SDK is only available to plugins that declare bot capabilities in their manifest. If your manifest does not include bot: { inline: true }, then sdk.bot will be null.
export const manifest: PluginManifest = {
name: "my-plugin",
version: "1.0.0",
description: "Plugin with bot capabilities",
bot: {
inline: true, // Enable inline query handling
callbacks: true, // Enable callback query handling
rateLimits: {
inlinePerMinute: 30, // Default: 30
callbackPerMinute: 60, // Default: 60
},
},
};Register handlers in your start() hook, not in tools(). The bot handlers are event-driven and persist for the plugin's lifetime.
Properties
| Property | Type | Description |
|---|---|---|
isAvailable | boolean | Whether the bot client is connected and ready. Always check before using other methods. |
username | string | The bot's Telegram username (e.g. "my_bot"). Used to construct inline links like @my_bot query. |
Methods
onInlineQuery(handler)
Register a handler for inline queries. When a user types @yourbot query in any chat, this handler is called with the query context. Return an array of InlineResult objects to display as results.
| Parameter | Type | Description |
|---|---|---|
handler | (ctx: InlineQueryContext) => Promise<InlineResult[]> | Async function that receives the query context and returns results. |
sdk.bot.onInlineQuery(async (ctx) => {
// ctx.query — the search text (prefix already stripped)
// ctx.queryId — Telegram query ID
// ctx.userId — user who triggered the query
// ctx.offset — pagination offset string
const results = await searchDatabase(ctx.query);
return results.map((item, i) => ({
id: String(i),
type: "article" as const,
title: item.name,
description: item.summary,
content: { text: item.fullText, parseMode: "HTML" as const },
keyboard: [[
{ text: "View Details", callback: `view:${item.id}`, style: "primary" },
]],
}));
});onCallback(pattern, handler)
Register a handler for callback queries matching a glob pattern. Patterns use * for wildcard matching. The callback data is automatically stripped of the plugin namespace prefix before matching.
| Parameter | Type | Description |
|---|---|---|
pattern | string | Glob pattern to match (e.g. "vote:*", "action:*:confirm"). |
handler | (ctx: CallbackContext) => Promise<void> | Async function called when a matching callback is received. |
sdk.bot.onCallback("vote:*", async (ctx) => {
// ctx.data — raw callback data (prefix stripped)
// ctx.match — regex match groups from the pattern
// ctx.userId — user who clicked
// ctx.username — optional username
// ctx.inlineMessageId — if from an inline message
// ctx.chatId — if from a regular message
// ctx.messageId — if from a regular message
const choice = ctx.data.split(":")[1]; // e.g. "yes" from "vote:yes"
await ctx.answer(`You voted: ${choice}`);
await ctx.editMessage(`Vote recorded: ${choice}`, {
keyboard: [[
{ text: "Vote recorded", callback: "noop", style: "success" },
]],
});
});onChosenResult(handler)
Register a handler for when a user selects an inline result. Requires inline feedback to be enabled via @BotFather (/setinlinefeedback).
| Parameter | Type | Description |
|---|---|---|
handler | (ctx: ChosenResultContext) => Promise<void> | Async function called when a user picks an inline result. |
sdk.bot.onChosenResult(async (ctx) => {
// ctx.resultId — the ID of the chosen result
// ctx.inlineMessageId — for editing the sent message later
// ctx.query — the original query text
sdk.log.info(`User chose result ${ctx.resultId} for query "${ctx.query}"`);
// You can edit the inline message after the user selects it
if (ctx.inlineMessageId) {
await sdk.bot.editInlineMessage(ctx.inlineMessageId, "Thanks for choosing this result!");
}
});editInlineMessage(inlineMessageId, text, opts?)
Edit a message that was sent via inline mode. You need the inlineMessageId from a callback or chosen result context.
| Parameter | Type | Description |
|---|---|---|
inlineMessageId | string | The inline message ID to edit. |
text | string | New message text. |
opts | { keyboard?: ButtonDef[][]; parseMode?: string } | Optional keyboard and parse mode. |
await sdk.bot.editInlineMessage(ctx.inlineMessageId, "Updated content!", {
parseMode: "HTML",
keyboard: [[
{ text: "Refresh", callback: "refresh:123", style: "primary" },
{ text: "Close", callback: "close:123", style: "danger" },
]],
});keyboard(rows)
Build a keyboard object from button definitions. Returns a BotKeyboard with methods for both GramJS (MTProto, with colors) and Grammy (Bot API, no colors) formats. Callback data is automatically prefixed with the plugin name.
| Parameter | Type | Description |
|---|---|---|
rows | ButtonDef[][] | Array of rows, each containing button definitions. |
| Returns | Description |
|---|---|
BotKeyboard | Object with .toTL(), .toGrammy(), and .rows properties. |
const kb = sdk.bot.keyboard([
[
{ text: "Confirm", callback: "confirm:42", style: "success" },
{ text: "Cancel", callback: "cancel:42", style: "danger" },
],
[
{ text: "Visit Website", url: "https://example.com" },
],
]);
// For GramJS (MTProto) — supports colored buttons
const tlMarkup = kb.toTL();
// For Grammy (Bot API) — standard buttons, no colors
const grammyKeyboard = kb.toGrammy();
// Access raw rows (callbacks already prefixed)
console.log(kb.rows);Styled Buttons
The Bot SDK supports colored inline keyboard buttons via GramJS Layer 222. Buttons are defined using the ButtonDef interface:
ButtonDef
| Property | Type | Required | Description |
|---|---|---|---|
text | string | Yes | Button label text displayed to the user. |
callback | string | No | Callback data sent when pressed. Auto-prefixed with plugin name. |
url | string | No | URL to open when pressed. |
copy | string | No | Text to copy to clipboard when pressed. |
style | ButtonStyle | No | Button color: "success" (green), "danger" (red), or "primary" (blue). |
Button Styles
| Style | Color | Use Case |
|---|---|---|
"success" | Green | Confirmations, positive actions, "yes" votes |
"danger" | Red | Deletions, cancellations, "no" votes, warnings |
"primary" | Blue | Primary actions, navigation, neutral selections |
Rendering Formats
Use .toTL() for GramJS Layer 222 (MTProto) which renders colored buttons natively. Use .toGrammy() for the standard Bot API fallback where colors are silently ignored.
// Green confirmation + red cancel
const confirmKeyboard = sdk.bot.keyboard([
[
{ text: "Confirm Payment", callback: "pay:confirm", style: "success" },
{ text: "Cancel", callback: "pay:cancel", style: "danger" },
],
]);
// Blue navigation row + copy button
const navKeyboard = sdk.bot.keyboard([
[
{ text: "Previous", callback: "page:prev", style: "primary" },
{ text: "Next", callback: "page:next", style: "primary" },
],
[
{ text: "Copy Address", copy: "EQDrjaLahLkMB-hMCmkzOyBuHJ186F-g..." },
],
]);Gotcha: Callback data is automatically prefixed with the plugin name to prevent collisions between plugins. If your plugin is named "dice-game" and you set callback: "roll:6", the actual callback data sent to Telegram is "dice-game:roll:6". The prefix is stripped back before your handler receives it, so your handler always sees "roll:6".
Type Definitions
interface BotManifest {
/** Enable inline query handling */
inline?: boolean;
/** Enable callback query handling */
callbacks?: boolean;
/** Rate limits */
rateLimits?: {
/** Max inline answers per minute (default: 30) */
inlinePerMinute?: number;
/** Max callback answers per minute (default: 60) */
callbackPerMinute?: number;
};
}type ButtonStyle = "success" | "danger" | "primary";
interface ButtonDef {
text: string;
callback?: string;
url?: string;
copy?: string;
style?: ButtonStyle;
}type InlineResultContent =
| { text: string; parseMode?: "HTML" | "Markdown" }
| { photoUrl: string; thumbUrl?: string; caption?: string }
| { gifUrl: string; thumbUrl?: string; caption?: string };
interface InlineResult {
id: string;
type: "article" | "photo" | "gif";
title: string;
description?: string;
thumbUrl?: string;
content: InlineResultContent;
keyboard?: ButtonDef[][];
}interface InlineQueryContext {
/** The query text (prefix already stripped) */
query: string;
/** Telegram query ID */
queryId: string;
/** User who triggered the query */
userId: number;
/** Pagination offset */
offset: string;
}interface CallbackContext {
/** Raw callback data (prefix already stripped) */
data: string;
/** Regex match groups (if pattern was used) */
match: string[];
/** User who clicked */
userId: number;
/** Username of the user */
username?: string;
/** Inline message ID (if from inline message) */
inlineMessageId?: string;
/** Chat ID (if from regular message) */
chatId?: string;
/** Message ID (if from regular message) */
messageId?: number;
/** Answer the callback query (toast/alert) */
answer(text?: string, alert?: boolean): Promise<void>;
/** Edit the message that contains the button */
editMessage(text: string, opts?: { keyboard?: ButtonDef[][]; parseMode?: string }): Promise<void>;
}interface ChosenResultContext {
/** The result ID that was chosen */
resultId: string;
/** Inline message ID (available if bot has inline feedback enabled) */
inlineMessageId?: string;
/** The query that was used */
query: string;
}interface BotKeyboard {
/** Get Grammy InlineKeyboard (Bot API, no colors) */
toGrammy(): unknown;
/** Get GramJS TL ReplyInlineMarkup (MTProto, with colors) */
toTL(): unknown;
/** Raw button definitions (with prefixed callbacks) */
rows: ButtonDef[][];
}interface BotSDK {
readonly isAvailable: boolean;
readonly username: string;
onInlineQuery(handler: (ctx: InlineQueryContext) => Promise<InlineResult[]>): void;
onCallback(pattern: string, handler: (ctx: CallbackContext) => Promise<void>): void;
onChosenResult(handler: (ctx: ChosenResultContext) => Promise<void>): void;
editInlineMessage(
inlineMessageId: string,
text: string,
opts?: { keyboard?: ButtonDef[][]; parseMode?: string }
): Promise<void>;
keyboard(rows: ButtonDef[][]): BotKeyboard;
}Rate Limiting
The Bot SDK enforces per-plugin rate limits to prevent abuse. Configure limits in your manifest's bot.rateLimits:
| Limit | Default | Description |
|---|---|---|
inlinePerMinute | 30 | Maximum inline query responses per minute. |
callbackPerMinute | 60 | Maximum callback query responses per minute. |
When a limit is exceeded, the handler is silently skipped. The user sees no response (inline results disappear, callback gets no answer). Set higher limits for high-traffic bots, but be mindful of Telegram's own API rate limits.
Complete Example: Dice Game Bot
A fully functional inline dice game where users roll dice via inline mode, see results with colored buttons, and can reroll.
import type { PluginSDK, SimpleToolDef, PluginManifest } from "@teleton-agent/sdk";
export const manifest: PluginManifest = {
name: "dice-game",
version: "1.0.0",
description: "Inline dice game with styled buttons",
bot: {
inline: true,
callbacks: true,
rateLimits: { inlinePerMinute: 60, callbackPerMinute: 120 },
},
};
function rollDice(): number {
return Math.floor(Math.random() * 6) + 1;
}
function diceEmoji(n: number): string {
return ["\u2680", "\u2681", "\u2682", "\u2683", "\u2684", "\u2685"][n - 1];
}
export const start = async (sdk: PluginSDK) => {
if (!sdk.bot) {
sdk.log.warn("Bot SDK not available, skipping inline setup");
return;
}
// Handle inline queries — show a "Roll Dice" option
sdk.bot.onInlineQuery(async (ctx) => {
const sides = parseInt(ctx.query) || 6;
const clamped = Math.min(Math.max(sides, 2), 20);
return [{
id: `roll-${clamped}`,
type: "article",
title: `Roll a d${clamped}`,
description: `Roll a ${clamped}-sided die and see the result`,
content: { text: `Rolling a d${clamped}...` },
keyboard: [[
{ text: `Roll d${clamped}`, callback: `roll:${clamped}`, style: "primary" },
]],
}];
});
// Handle the Roll button press
sdk.bot.onCallback("roll:*", async (ctx) => {
const sides = parseInt(ctx.data.split(":")[1]) || 6;
const result = Math.floor(Math.random() * sides) + 1;
const emoji = sides === 6 ? ` ${diceEmoji(result)}` : "";
await ctx.editMessage(`You rolled a d${sides}: **${result}**${emoji}`, {
parseMode: "Markdown",
keyboard: [
[
{ text: `Roll again (d${sides})`, callback: `roll:${sides}`, style: "success" },
{ text: "Change dice", callback: "pick", style: "primary" },
],
[
{ text: `Share result: ${result}`, copy: `I rolled a ${result} on a d${sides}!` },
],
],
});
await ctx.answer(`You rolled ${result}!`);
});
// Handle dice picker
sdk.bot.onCallback("pick", async (ctx) => {
await ctx.editMessage("Choose your dice:", {
keyboard: [
[
{ text: "d4", callback: "roll:4", style: "primary" },
{ text: "d6", callback: "roll:6", style: "primary" },
{ text: "d8", callback: "roll:8", style: "primary" },
],
[
{ text: "d10", callback: "roll:10", style: "primary" },
{ text: "d12", callback: "roll:12", style: "primary" },
{ text: "d20", callback: "roll:20", style: "success" },
],
],
});
await ctx.answer();
});
// Track which results users select
sdk.bot.onChosenResult(async (ctx) => {
sdk.log.info(`User chose "${ctx.resultId}" from query "${ctx.query}"`);
});
sdk.log.info(`Dice game inline bot ready: @${sdk.bot.username}`);
};
// Optionally export agent tools alongside the inline bot
export const tools = (sdk: PluginSDK): SimpleToolDef[] => [
{
name: "dice_roll",
description: "Roll dice (for use in chat without inline mode)",
parameters: {
type: "object",
properties: {
sides: { type: "number", description: "Number of sides (default: 6)" },
},
},
async execute(params) {
const sides = params.sides || 6;
const result = Math.floor(Math.random() * sides) + 1;
return { result, sides, display: `Rolled d${sides}: ${result}` };
},
},
];