Agent skill
telegram-bot
Build Telegram bots with modern frameworks. Use when: creating Telegram bot, chatbot, notification bot. Triggers: "telegram", "tg bot", "телеграм бот", "teloxide", "aiogram".
Install this agent skill to your Project
npx add-skill https://github.com/timequity/vibe-coder/tree/main/skills/telegram-bot
SKILL.md
Telegram Bot Development
Project Protection Setup
MANDATORY before writing any code:
# 1. Create .gitignore
cat >> .gitignore << 'EOF'
# Build
target/
node_modules/
__pycache__/
*.pyc
dist/
# Secrets - CRITICAL for bots!
.env
.env.*
!.env.example
*.key
bot_token.txt
# IDE
.idea/
.vscode/
.DS_Store
EOF
# 2. Setup pre-commit hooks
cat > .pre-commit-config.yaml << 'EOF'
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v5.0.0
hooks:
- id: detect-private-key
- id: check-added-large-files
- repo: https://github.com/gitleaks/gitleaks
rev: v8.21.2
hooks:
- id: gitleaks
EOF
pre-commit install
Why critical for bots: Bot tokens give FULL access to your bot. Leaked token = compromised bot.
Stack Options
| Language | Framework | Best For |
|---|---|---|
| Rust | teloxide | Performance, type safety |
| Python | aiogram 3.x | Rapid development, async |
| Node | grammY / telegraf | JS ecosystem |
Quick Start
Rust (teloxide)
# Cargo.toml
[dependencies]
teloxide = { version = "0.13", features = ["macros"] }
tokio = { version = "1", features = ["full"] }
log = "0.4"
pretty_env_logger = "0.5"
use teloxide::prelude::*;
#[tokio::main]
async fn main() {
pretty_env_logger::init();
log::info!("Starting bot...");
let bot = Bot::from_env();
teloxide::repl(bot, |bot: Bot, msg: Message| async move {
bot.send_message(msg.chat.id, "Hello!").await?;
Ok(())
})
.await;
}
Python (aiogram 3.x)
# requirements.txt
aiogram>=3.0
import asyncio
from aiogram import Bot, Dispatcher, types
from aiogram.filters import Command
bot = Bot(token="BOT_TOKEN")
dp = Dispatcher()
@dp.message(Command("start"))
async def start(message: types.Message):
await message.answer("Hello!")
@dp.message()
async def echo(message: types.Message):
await message.answer(message.text)
async def main():
await dp.start_polling(bot)
if __name__ == "__main__":
asyncio.run(main())
Node (grammY)
// package.json: "grammy": "^1.21"
import { Bot } from "grammy";
const bot = new Bot(process.env.BOT_TOKEN!);
bot.command("start", (ctx) => ctx.reply("Hello!"));
bot.on("message", (ctx) => ctx.reply(ctx.message.text ?? ""));
bot.start();
Command Handlers
Rust
use teloxide::{prelude::*, utils::command::BotCommands};
#[derive(BotCommands, Clone)]
#[command(rename_rule = "lowercase")]
enum Command {
#[command(description = "Start the bot")]
Start,
#[command(description = "Get help")]
Help,
#[command(description = "Echo text")]
Echo(String),
}
async fn answer(bot: Bot, msg: Message, cmd: Command) -> ResponseResult<()> {
match cmd {
Command::Start => bot.send_message(msg.chat.id, "Welcome!").await?,
Command::Help => bot.send_message(msg.chat.id, Command::descriptions().to_string()).await?,
Command::Echo(text) => bot.send_message(msg.chat.id, text).await?,
};
Ok(())
}
Python
from aiogram.filters import Command
@dp.message(Command("start"))
async def cmd_start(message: types.Message):
await message.answer("Welcome!")
@dp.message(Command("help"))
async def cmd_help(message: types.Message):
await message.answer("Available commands:\n/start - Start\n/help - Help")
Inline Keyboards
Rust
use teloxide::types::{InlineKeyboardButton, InlineKeyboardMarkup};
let keyboard = InlineKeyboardMarkup::new(vec![
vec![
InlineKeyboardButton::callback("Option 1", "opt_1"),
InlineKeyboardButton::callback("Option 2", "opt_2"),
],
vec![
InlineKeyboardButton::url("Website", "https://example.com".parse().unwrap()),
],
]);
bot.send_message(chat_id, "Choose:")
.reply_markup(keyboard)
.await?;
Python
from aiogram.types import InlineKeyboardMarkup, InlineKeyboardButton
keyboard = InlineKeyboardMarkup(inline_keyboard=[
[
InlineKeyboardButton(text="Option 1", callback_data="opt_1"),
InlineKeyboardButton(text="Option 2", callback_data="opt_2"),
],
[
InlineKeyboardButton(text="Website", url="https://example.com"),
]
])
await message.answer("Choose:", reply_markup=keyboard)
Callback Handling
Rust
#[derive(Clone)]
enum CallbackAction {
Option1,
Option2,
}
async fn callback_handler(
bot: Bot,
q: CallbackQuery,
) -> ResponseResult<()> {
if let Some(data) = &q.data {
let text = match data.as_str() {
"opt_1" => "You chose Option 1",
"opt_2" => "You chose Option 2",
_ => "Unknown",
};
bot.answer_callback_query(&q.id).await?;
if let Some(msg) = q.message {
bot.edit_message_text(msg.chat.id, msg.id, text).await?;
}
}
Ok(())
}
Python
@dp.callback_query(lambda c: c.data.startswith("opt_"))
async def process_callback(callback: types.CallbackQuery):
if callback.data == "opt_1":
text = "You chose Option 1"
else:
text = "You chose Option 2"
await callback.answer() # Remove loading state
await callback.message.edit_text(text)
FSM (Finite State Machine) for Dialogs
Python (aiogram)
from aiogram.fsm.context import FSMContext
from aiogram.fsm.state import State, StatesGroup
class Form(StatesGroup):
name = State()
email = State()
confirm = State()
@dp.message(Command("register"))
async def start_registration(message: types.Message, state: FSMContext):
await state.set_state(Form.name)
await message.answer("Enter your name:")
@dp.message(Form.name)
async def process_name(message: types.Message, state: FSMContext):
await state.update_data(name=message.text)
await state.set_state(Form.email)
await message.answer("Enter your email:")
@dp.message(Form.email)
async def process_email(message: types.Message, state: FSMContext):
await state.update_data(email=message.text)
data = await state.get_data()
await state.clear()
await message.answer(f"Registered: {data['name']} ({data['email']})")
Rust (teloxide dialogue)
use teloxide::dispatching::dialogue::{InMemStorage, Dialogue};
type MyDialogue = Dialogue<State, InMemStorage<State>>;
#[derive(Clone, Default)]
pub enum State {
#[default]
Start,
ReceiveName,
ReceiveEmail { name: String },
}
async fn start(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult {
bot.send_message(msg.chat.id, "Enter your name:").await?;
dialogue.update(State::ReceiveName).await?;
Ok(())
}
async fn receive_name(bot: Bot, dialogue: MyDialogue, msg: Message) -> HandlerResult {
let name = msg.text().unwrap_or_default().to_string();
bot.send_message(msg.chat.id, "Enter your email:").await?;
dialogue.update(State::ReceiveEmail { name }).await?;
Ok(())
}
Webhooks vs Polling
| Method | Pros | Cons | Use When |
|---|---|---|---|
| Polling | Simple setup, works locally | Less efficient, delay | Development, small bots |
| Webhooks | Real-time, efficient | Needs HTTPS, public URL | Production |
Webhook Setup (Rust)
use teloxide::dispatching::update_listeners::webhooks;
let addr = ([0, 0, 0, 0], 8443).into();
let url = "https://your-domain.com/webhook".parse().unwrap();
let listener = webhooks::axum(bot.clone(), webhooks::Options::new(addr, url))
.await
.unwrap();
teloxide::repl_with_listener(bot, handler, listener).await;
Database Integration
SQLite with SQLx (Rust)
use sqlx::SqlitePool;
struct AppState {
db: SqlitePool,
}
async fn save_user(pool: &SqlitePool, user_id: i64, username: &str) -> Result<()> {
sqlx::query!(
"INSERT OR REPLACE INTO users (id, username) VALUES (?, ?)",
user_id, username
)
.execute(pool)
.await?;
Ok(())
}
Environment & Security
# .env (NEVER commit!)
TELOXIDE_TOKEN=123456:ABC-DEF...
DATABASE_URL=sqlite:bot.db
# .gitignore
.env
*.db
Common Pitfalls
| Pitfall | Solution |
|---|---|
| Token in code | Use environment variables |
| No error handling | Wrap handlers in try/catch |
| Blocking in async | Use tokio::spawn for heavy work |
| No rate limiting | Respect Telegram limits (30 msg/sec) |
| Large media sync | Use file_id, not re-uploading |
Testing
Rust (teloxide)
#[cfg(test)]
mod tests {
use super::*;
// Unit test for message processing logic
#[test]
fn test_parse_command() {
let result = parse_expense("/add 100 food");
assert_eq!(result.amount, 100.0);
assert_eq!(result.category, "food");
}
// Integration test with mock bot
#[tokio::test]
async fn test_start_command_returns_welcome() {
// Use teloxide_tests or mock the bot
let response = handle_start_command().await;
assert!(response.contains("Welcome"));
}
}
Python (aiogram)
import pytest
from unittest.mock import AsyncMock, MagicMock
@pytest.fixture
def mock_message():
message = MagicMock()
message.answer = AsyncMock()
message.text = "/start"
message.from_user.id = 123
return message
@pytest.mark.asyncio
async def test_start_handler(mock_message):
await cmd_start(mock_message)
mock_message.answer.assert_called_once()
assert "Welcome" in mock_message.answer.call_args[0][0]
@pytest.mark.asyncio
async def test_fsm_state_transition(mock_message, state):
await start_registration(mock_message, state)
current_state = await state.get_state()
assert current_state == Form.name
Node (grammY)
import { describe, it, expect, vi } from 'vitest';
describe('Bot handlers', () => {
it('responds to /start', async () => {
const ctx = {
reply: vi.fn(),
message: { text: '/start' },
};
await startHandler(ctx);
expect(ctx.reply).toHaveBeenCalledWith(expect.stringContaining('Welcome'));
});
});
TDD Workflow
1. Task[tdd-test-writer]: "Create /start command handler"
→ Writes test that expects welcome message
→ cargo test / pytest / npm test → FAILS (RED)
2. Task[rust-developer]: "Implement /start handler"
→ Implements minimal code
→ cargo test → PASSES (GREEN)
3. Repeat for each command/feature
4. Task[code-reviewer]: "Review bot implementation"
→ Checks security, error handling, patterns
Security Checklist
- Token in environment variable (never hardcoded)
-
.envin.gitignore - pre-commit hooks with gitleaks
- Input validation (no command injection)
- Rate limiting for user requests
- Webhook URL uses HTTPS
- No sensitive data in logs
- User data encrypted if stored
Project Structure
telegram-bot/
├── src/
│ ├── main.rs
│ ├── handlers/
│ │ ├── mod.rs
│ │ ├── commands.rs
│ │ └── callbacks.rs
│ ├── state.rs # FSM states
│ └── db.rs # Database
├── tests/
│ └── integration.rs # Integration tests
├── Cargo.toml
├── .env.example # Template (committed)
├── .env # Real secrets (NOT committed)
└── .gitignore
Recommended Agent Skills
Expand your agent's capabilities with these related and highly-rated skills.
mvp-help
Help and documentation for Idea to MVP plugin. Use when: user asks about building MVPs, vibe coding, or available commands. Triggers: "help", "what can you do", "mvp help", "how to build".
verification-gate
Hidden quality gate that runs before showing "Done!" to user - ensures all tests pass, build succeeds, and requirements met before claiming completion
brainstorming
Refine ideas into detailed designs through Socratic dialogue. Use when: user has rough idea, needs to clarify requirements, explore approaches. Triggers: "brainstorm", "discuss idea", "I'm thinking about", "what if", "help me think through", "explore options", "/brainstorm".
subagent-creator
Guide for creating effective subagents (custom agents). Use when users want to create a new subagent that can be dispatched via Task tool for autonomous work. Covers frontmatter fields (name, description, tools, model, permissionMode, skills), prompt design, and when to use subagents vs skills.
backend-rust
Modern Rust backend with Axum, SQLx, tokio + CI/CD automation. Use when: building Rust APIs, high-performance services, or needing build/test/lint/audit automation. Triggers: "axum", "rust backend", "rust api", "sqlx", "tokio", "cargo build", "cargo test", "clippy", "rustfmt", "cargo-audit", "cross-compile", "rust ci", "release build", "rust security", "shuttle", "actix".
test-driven-development
Write failing test first, then minimal code to pass. Red-Green-Refactor cycle. Use when: implementing features, fixing bugs, refactoring code. Triggers: "implement", "add feature", "fix bug", "tdd", "test first", "write tests", "test-driven".
Didn't find tool you were looking for?