[{"data":1,"prerenderedAt":1153},["ShallowReactive",2],{"blog-en-aiogram-3-tutorial-2025":3},{"id":4,"title":5,"body":6,"date":1137,"description":1138,"extension":1139,"meta":1140,"navigation":196,"path":1141,"readTime":1142,"seo":1143,"slug":1144,"stem":1145,"tags":1146,"__hash__":1152},"blogEn\u002Fen\u002Fblog\u002Faiogram-3-tutorial-2025.md","Aiogram 3 Tutorial: Build a Telegram Bot in Python (2025)",{"type":7,"value":8,"toc":1125},"minimark",[9,13,18,21,68,75,79,104,119,122,130,136,154,158,267,274,278,289,358,386,389,393,400,480,487,491,550,606,613,617,620,659,834,848,852,900,903,907,910,990,1053,1056,1060,1063,1103,1118,1121],[10,11,12],"p",{},"Aiogram 3 is the async-first Python framework for Telegram bots. I've used it on 20+ production projects. Here's everything you need to go from nothing to a deployed bot, without wading through outdated tutorials.",[14,15,17],"h2",{"id":16},"why-aiogram-3-specifically","Why Aiogram 3 specifically",[10,19,20],{},"There are a few Python Telegram bot libraries. Aiogram 3 wins for production work because:",[22,23,24,32,38,56,62],"ul",{},[25,26,27,31],"li",{},[28,29,30],"strong",{},"Async-first"," — built on asyncio, handles thousands of updates efficiently",[25,33,34,37],{},[28,35,36],{},"Router system"," — split your handlers across modules cleanly",[25,39,40,43,44,48,49,48,52,55],{},[28,41,42],{},"Magic filters"," — ",[45,46,47],"code",{},"F.text == \"hello\"",", ",[45,50,51],{},"F.photo",[45,53,54],{},"F.from_user.id == 123456"," — expressive and readable",[25,57,58,61],{},[28,59,60],{},"FSM"," — built-in finite state machine for multi-step dialogs",[25,63,64,67],{},[28,65,66],{},"Type safety"," — full type hints throughout",[10,69,70,71,74],{},"The older ",[45,72,73],{},"python-telegram-bot"," works too, but Aiogram 3's architecture is cleaner for anything beyond toy projects.",[14,76,78],{"id":77},"setup","Setup",[80,81,86],"pre",{"className":82,"code":83,"language":84,"meta":85,"style":85},"language-bash shiki shiki-themes github-light github-dark","pip install aiogram\n","bash","",[45,87,88],{"__ignoreMap":85},[89,90,93,97,101],"span",{"class":91,"line":92},"line",1,[89,94,96],{"class":95},"sScJk","pip",[89,98,100],{"class":99},"sZZnC"," install",[89,102,103],{"class":99}," aiogram\n",[10,105,106,107,114,115,118],{},"Get a bot token from ",[108,109,113],"a",{"href":110,"rel":111},"https:\u002F\u002Ft.me\u002FBotFather",[112],"nofollow","@BotFather"," — send ",[45,116,117],{},"\u002Fnewbot",", follow the prompts.",[10,120,121],{},"Project structure I use:",[80,123,128],{"className":124,"code":126,"language":127},[125],"language-text","mybot\u002F\n├── bot.py          # entry point\n├── config.py       # token + settings\n├── handlers\u002F\n│   ├── start.py\n│   └── orders.py\n├── keyboards\u002F\n│   └── kb.py\n└── states\u002F\n    └── states.py\n","text",[45,129,126],{"__ignoreMap":85},[10,131,132,135],{},[45,133,134],{},"config.py",":",[80,137,141],{"className":138,"code":139,"language":140,"meta":85,"style":85},"language-python shiki shiki-themes github-light github-dark","import os\nBOT_TOKEN = os.getenv(\"BOT_TOKEN\", \"your_token_here\")\n","python",[45,142,143,148],{"__ignoreMap":85},[89,144,145],{"class":91,"line":92},[89,146,147],{},"import os\n",[89,149,151],{"class":91,"line":150},2,[89,152,153],{},"BOT_TOKEN = os.getenv(\"BOT_TOKEN\", \"your_token_here\")\n",[14,155,157],{"id":156},"first-bot-start-handler","First bot: \u002Fstart handler",[80,159,161],{"className":138,"code":160,"language":140,"meta":85,"style":85},"# bot.py\nimport asyncio\nfrom aiogram import Bot, Dispatcher\nfrom aiogram.filters import CommandStart\nfrom aiogram.types import Message\n\nbot = Bot(token=\"YOUR_TOKEN\")\ndp = Dispatcher()\n\n@dp.message(CommandStart())\nasync def cmd_start(message: Message):\n    await message.answer(f\"Hello, {message.from_user.first_name}!\")\n\nasync def main():\n    await dp.start_polling(bot)\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n",[45,162,163,168,173,179,185,191,198,204,210,215,221,227,233,238,244,250,255,261],{"__ignoreMap":85},[89,164,165],{"class":91,"line":92},[89,166,167],{},"# bot.py\n",[89,169,170],{"class":91,"line":150},[89,171,172],{},"import asyncio\n",[89,174,176],{"class":91,"line":175},3,[89,177,178],{},"from aiogram import Bot, Dispatcher\n",[89,180,182],{"class":91,"line":181},4,[89,183,184],{},"from aiogram.filters import CommandStart\n",[89,186,188],{"class":91,"line":187},5,[89,189,190],{},"from aiogram.types import Message\n",[89,192,194],{"class":91,"line":193},6,[89,195,197],{"emptyLinePlaceholder":196},true,"\n",[89,199,201],{"class":91,"line":200},7,[89,202,203],{},"bot = Bot(token=\"YOUR_TOKEN\")\n",[89,205,207],{"class":91,"line":206},8,[89,208,209],{},"dp = Dispatcher()\n",[89,211,213],{"class":91,"line":212},9,[89,214,197],{"emptyLinePlaceholder":196},[89,216,218],{"class":91,"line":217},10,[89,219,220],{},"@dp.message(CommandStart())\n",[89,222,224],{"class":91,"line":223},11,[89,225,226],{},"async def cmd_start(message: Message):\n",[89,228,230],{"class":91,"line":229},12,[89,231,232],{},"    await message.answer(f\"Hello, {message.from_user.first_name}!\")\n",[89,234,236],{"class":91,"line":235},13,[89,237,197],{"emptyLinePlaceholder":196},[89,239,241],{"class":91,"line":240},14,[89,242,243],{},"async def main():\n",[89,245,247],{"class":91,"line":246},15,[89,248,249],{},"    await dp.start_polling(bot)\n",[89,251,253],{"class":91,"line":252},16,[89,254,197],{"emptyLinePlaceholder":196},[89,256,258],{"class":91,"line":257},17,[89,259,260],{},"if __name__ == \"__main__\":\n",[89,262,264],{"class":91,"line":263},18,[89,265,266],{},"    asyncio.run(main())\n",[10,268,269,270,273],{},"Run it, send ",[45,271,272],{},"\u002Fstart"," to your bot. That's the base.",[14,275,277],{"id":276},"routers-keeping-your-code-organized","Routers: keeping your code organized",[10,279,280,281,284,285,288],{},"In Aiogram 3, ",[45,282,283],{},"Router"," is the key abstraction. Instead of registering everything on the ",[45,286,287],{},"Dispatcher",", you create routers per module and include them:",[80,290,292],{"className":138,"code":291,"language":140,"meta":85,"style":85},"# handlers\u002Fstart.py\nfrom aiogram import Router, F\nfrom aiogram.filters import CommandStart\nfrom aiogram.types import Message\n\nrouter = Router()\n\n@router.message(CommandStart())\nasync def cmd_start(message: Message):\n    await message.answer(\"Welcome!\")\n\n@router.message(F.text == \"help\")\nasync def help_text(message: Message):\n    await message.answer(\"Here's how this bot works...\")\n",[45,293,294,299,304,308,312,316,321,325,330,334,339,343,348,353],{"__ignoreMap":85},[89,295,296],{"class":91,"line":92},[89,297,298],{},"# handlers\u002Fstart.py\n",[89,300,301],{"class":91,"line":150},[89,302,303],{},"from aiogram import Router, F\n",[89,305,306],{"class":91,"line":175},[89,307,184],{},[89,309,310],{"class":91,"line":181},[89,311,190],{},[89,313,314],{"class":91,"line":187},[89,315,197],{"emptyLinePlaceholder":196},[89,317,318],{"class":91,"line":193},[89,319,320],{},"router = Router()\n",[89,322,323],{"class":91,"line":200},[89,324,197],{"emptyLinePlaceholder":196},[89,326,327],{"class":91,"line":206},[89,328,329],{},"@router.message(CommandStart())\n",[89,331,332],{"class":91,"line":212},[89,333,226],{},[89,335,336],{"class":91,"line":217},[89,337,338],{},"    await message.answer(\"Welcome!\")\n",[89,340,341],{"class":91,"line":223},[89,342,197],{"emptyLinePlaceholder":196},[89,344,345],{"class":91,"line":229},[89,346,347],{},"@router.message(F.text == \"help\")\n",[89,349,350],{"class":91,"line":235},[89,351,352],{},"async def help_text(message: Message):\n",[89,354,355],{"class":91,"line":240},[89,356,357],{},"    await message.answer(\"Here's how this bot works...\")\n",[80,359,361],{"className":138,"code":360,"language":140,"meta":85,"style":85},"# bot.py\nfrom handlers import start, orders\n\ndp.include_router(start.router)\ndp.include_router(orders.router)\n",[45,362,363,367,372,376,381],{"__ignoreMap":85},[89,364,365],{"class":91,"line":92},[89,366,167],{},[89,368,369],{"class":91,"line":150},[89,370,371],{},"from handlers import start, orders\n",[89,373,374],{"class":91,"line":175},[89,375,197],{"emptyLinePlaceholder":196},[89,377,378],{"class":91,"line":181},[89,379,380],{},"dp.include_router(start.router)\n",[89,382,383],{"class":91,"line":187},[89,384,385],{},"dp.include_router(orders.router)\n",[10,387,388],{},"This keeps large bots maintainable. Each handler file stays focused.",[14,390,392],{"id":391},"magic-filters-f","Magic filters: F.",[10,394,395,396,399],{},"The ",[45,397,398],{},"F"," object is where Aiogram 3 really shines:",[80,401,403],{"className":138,"code":402,"language":140,"meta":85,"style":85},"from aiogram import F\n\n# Match exact text\n@router.message(F.text == \"Buy\")\n\n# Match photo messages\n@router.message(F.photo)\n\n# Match specific user\n@router.message(F.from_user.id == 123456789)\n\n# Combine filters\n@router.message(F.text.startswith(\"order:\") & F.from_user.is_premium)\n\n# Negate\n@router.message(~F.text.in_({\"stop\", \"cancel\"}))\n",[45,404,405,410,414,419,424,428,433,438,442,447,452,456,461,466,470,475],{"__ignoreMap":85},[89,406,407],{"class":91,"line":92},[89,408,409],{},"from aiogram import F\n",[89,411,412],{"class":91,"line":150},[89,413,197],{"emptyLinePlaceholder":196},[89,415,416],{"class":91,"line":175},[89,417,418],{},"# Match exact text\n",[89,420,421],{"class":91,"line":181},[89,422,423],{},"@router.message(F.text == \"Buy\")\n",[89,425,426],{"class":91,"line":187},[89,427,197],{"emptyLinePlaceholder":196},[89,429,430],{"class":91,"line":193},[89,431,432],{},"# Match photo messages\n",[89,434,435],{"class":91,"line":200},[89,436,437],{},"@router.message(F.photo)\n",[89,439,440],{"class":91,"line":206},[89,441,197],{"emptyLinePlaceholder":196},[89,443,444],{"class":91,"line":212},[89,445,446],{},"# Match specific user\n",[89,448,449],{"class":91,"line":217},[89,450,451],{},"@router.message(F.from_user.id == 123456789)\n",[89,453,454],{"class":91,"line":223},[89,455,197],{"emptyLinePlaceholder":196},[89,457,458],{"class":91,"line":229},[89,459,460],{},"# Combine filters\n",[89,462,463],{"class":91,"line":235},[89,464,465],{},"@router.message(F.text.startswith(\"order:\") & F.from_user.is_premium)\n",[89,467,468],{"class":91,"line":240},[89,469,197],{"emptyLinePlaceholder":196},[89,471,472],{"class":91,"line":246},[89,473,474],{},"# Negate\n",[89,476,477],{"class":91,"line":252},[89,478,479],{},"@router.message(~F.text.in_({\"stop\", \"cancel\"}))\n",[10,481,482,483,486],{},"No more ",[45,484,485],{},"if message.text == \"Buy\":"," inside catch-all handlers. Filters are declared, readable, and composable.",[14,488,490],{"id":489},"inline-keyboards-and-callbacks","Inline keyboards and callbacks",[80,492,494],{"className":138,"code":493,"language":140,"meta":85,"style":85},"# keyboards\u002Fkb.py\nfrom aiogram.utils.keyboard import InlineKeyboardBuilder\nfrom aiogram.types import InlineKeyboardMarkup\n\ndef main_kb() -> InlineKeyboardMarkup:\n    b = InlineKeyboardBuilder()\n    b.button(text=\"📦 Products\", callback_data=\"products\")\n    b.button(text=\"📋 My orders\", callback_data=\"my_orders\")\n    b.button(text=\"💬 Contact\", url=\"https:\u002F\u002Ft.me\u002Fyourusername\")\n    b.adjust(2, 1)  # 2 buttons in row 1, 1 in row 2\n    return b.as_markup()\n",[45,495,496,501,506,511,515,520,525,530,535,540,545],{"__ignoreMap":85},[89,497,498],{"class":91,"line":92},[89,499,500],{},"# keyboards\u002Fkb.py\n",[89,502,503],{"class":91,"line":150},[89,504,505],{},"from aiogram.utils.keyboard import InlineKeyboardBuilder\n",[89,507,508],{"class":91,"line":175},[89,509,510],{},"from aiogram.types import InlineKeyboardMarkup\n",[89,512,513],{"class":91,"line":181},[89,514,197],{"emptyLinePlaceholder":196},[89,516,517],{"class":91,"line":187},[89,518,519],{},"def main_kb() -> InlineKeyboardMarkup:\n",[89,521,522],{"class":91,"line":193},[89,523,524],{},"    b = InlineKeyboardBuilder()\n",[89,526,527],{"class":91,"line":200},[89,528,529],{},"    b.button(text=\"📦 Products\", callback_data=\"products\")\n",[89,531,532],{"class":91,"line":206},[89,533,534],{},"    b.button(text=\"📋 My orders\", callback_data=\"my_orders\")\n",[89,536,537],{"class":91,"line":212},[89,538,539],{},"    b.button(text=\"💬 Contact\", url=\"https:\u002F\u002Ft.me\u002Fyourusername\")\n",[89,541,542],{"class":91,"line":217},[89,543,544],{},"    b.adjust(2, 1)  # 2 buttons in row 1, 1 in row 2\n",[89,546,547],{"class":91,"line":223},[89,548,549],{},"    return b.as_markup()\n",[80,551,553],{"className":138,"code":552,"language":140,"meta":85,"style":85},"# handler\nfrom aiogram.types import CallbackQuery\n\n@router.message(CommandStart())\nasync def cmd_start(message: Message):\n    await message.answer(\"Choose:\", reply_markup=main_kb())\n\n@router.callback_query(F.data == \"products\")\nasync def show_products(callback: CallbackQuery):\n    await callback.answer()  # dismiss the loading spinner\n    await callback.message.edit_text(\"Here are our products...\", reply_markup=products_kb())\n",[45,554,555,560,565,569,573,577,582,586,591,596,601],{"__ignoreMap":85},[89,556,557],{"class":91,"line":92},[89,558,559],{},"# handler\n",[89,561,562],{"class":91,"line":150},[89,563,564],{},"from aiogram.types import CallbackQuery\n",[89,566,567],{"class":91,"line":175},[89,568,197],{"emptyLinePlaceholder":196},[89,570,571],{"class":91,"line":181},[89,572,329],{},[89,574,575],{"class":91,"line":187},[89,576,226],{},[89,578,579],{"class":91,"line":193},[89,580,581],{},"    await message.answer(\"Choose:\", reply_markup=main_kb())\n",[89,583,584],{"class":91,"line":200},[89,585,197],{"emptyLinePlaceholder":196},[89,587,588],{"class":91,"line":206},[89,589,590],{},"@router.callback_query(F.data == \"products\")\n",[89,592,593],{"class":91,"line":212},[89,594,595],{},"async def show_products(callback: CallbackQuery):\n",[89,597,598],{"class":91,"line":217},[89,599,600],{},"    await callback.answer()  # dismiss the loading spinner\n",[89,602,603],{"class":91,"line":223},[89,604,605],{},"    await callback.message.edit_text(\"Here are our products...\", reply_markup=products_kb())\n",[10,607,608,609,612],{},"Always call ",[45,610,611],{},"callback.answer()"," first — without it, the button stays in loading state for 30 seconds.",[14,614,616],{"id":615},"fsm-multi-step-dialogs","FSM: multi-step dialogs",[10,618,619],{},"FSM (Finite State Machine) is how you handle flows that span multiple messages — order forms, registration, surveys.",[80,621,623],{"className":138,"code":622,"language":140,"meta":85,"style":85},"# states\u002Fstates.py\nfrom aiogram.fsm.state import State, StatesGroup\n\nclass OrderForm(StatesGroup):\n    waiting_name = State()\n    waiting_phone = State()\n    waiting_description = State()\n",[45,624,625,630,635,639,644,649,654],{"__ignoreMap":85},[89,626,627],{"class":91,"line":92},[89,628,629],{},"# states\u002Fstates.py\n",[89,631,632],{"class":91,"line":150},[89,633,634],{},"from aiogram.fsm.state import State, StatesGroup\n",[89,636,637],{"class":91,"line":175},[89,638,197],{"emptyLinePlaceholder":196},[89,640,641],{"class":91,"line":181},[89,642,643],{},"class OrderForm(StatesGroup):\n",[89,645,646],{"class":91,"line":187},[89,647,648],{},"    waiting_name = State()\n",[89,650,651],{"class":91,"line":193},[89,652,653],{},"    waiting_phone = State()\n",[89,655,656],{"class":91,"line":200},[89,657,658],{},"    waiting_description = State()\n",[80,660,662],{"className":138,"code":661,"language":140,"meta":85,"style":85},"# handlers\u002Forders.py\nfrom aiogram.fsm.context import FSMContext\nfrom states.states import OrderForm\n\n@router.callback_query(F.data == \"new_order\")\nasync def start_order(callback: CallbackQuery, state: FSMContext):\n    await state.set_state(OrderForm.waiting_name)\n    await callback.answer()\n    await callback.message.answer(\"What's your name?\")\n\n@router.message(OrderForm.waiting_name)\nasync def got_name(message: Message, state: FSMContext):\n    await state.update_data(name=message.text)\n    await state.set_state(OrderForm.waiting_phone)\n    await message.answer(\"Your phone number:\")\n\n@router.message(OrderForm.waiting_phone)\nasync def got_phone(message: Message, state: FSMContext):\n    await state.update_data(phone=message.text)\n    await state.set_state(OrderForm.waiting_description)\n    await message.answer(\"Describe what you need:\")\n\n@router.message(OrderForm.waiting_description)\nasync def got_description(message: Message, state: FSMContext):\n    data = await state.get_data()\n    await state.clear()\n    await message.answer(\n        f\"✅ Order received!\\n\"\n        f\"Name: {data['name']}\\n\"\n        f\"Phone: {data['phone']}\\n\"\n        f\"Task: {message.text}\"\n    )\n",[45,663,664,669,674,679,683,688,693,698,703,708,712,717,722,727,732,737,741,746,751,757,763,769,774,780,786,792,798,804,810,816,822,828],{"__ignoreMap":85},[89,665,666],{"class":91,"line":92},[89,667,668],{},"# handlers\u002Forders.py\n",[89,670,671],{"class":91,"line":150},[89,672,673],{},"from aiogram.fsm.context import FSMContext\n",[89,675,676],{"class":91,"line":175},[89,677,678],{},"from states.states import OrderForm\n",[89,680,681],{"class":91,"line":181},[89,682,197],{"emptyLinePlaceholder":196},[89,684,685],{"class":91,"line":187},[89,686,687],{},"@router.callback_query(F.data == \"new_order\")\n",[89,689,690],{"class":91,"line":193},[89,691,692],{},"async def start_order(callback: CallbackQuery, state: FSMContext):\n",[89,694,695],{"class":91,"line":200},[89,696,697],{},"    await state.set_state(OrderForm.waiting_name)\n",[89,699,700],{"class":91,"line":206},[89,701,702],{},"    await callback.answer()\n",[89,704,705],{"class":91,"line":212},[89,706,707],{},"    await callback.message.answer(\"What's your name?\")\n",[89,709,710],{"class":91,"line":217},[89,711,197],{"emptyLinePlaceholder":196},[89,713,714],{"class":91,"line":223},[89,715,716],{},"@router.message(OrderForm.waiting_name)\n",[89,718,719],{"class":91,"line":229},[89,720,721],{},"async def got_name(message: Message, state: FSMContext):\n",[89,723,724],{"class":91,"line":235},[89,725,726],{},"    await state.update_data(name=message.text)\n",[89,728,729],{"class":91,"line":240},[89,730,731],{},"    await state.set_state(OrderForm.waiting_phone)\n",[89,733,734],{"class":91,"line":246},[89,735,736],{},"    await message.answer(\"Your phone number:\")\n",[89,738,739],{"class":91,"line":252},[89,740,197],{"emptyLinePlaceholder":196},[89,742,743],{"class":91,"line":257},[89,744,745],{},"@router.message(OrderForm.waiting_phone)\n",[89,747,748],{"class":91,"line":263},[89,749,750],{},"async def got_phone(message: Message, state: FSMContext):\n",[89,752,754],{"class":91,"line":753},19,[89,755,756],{},"    await state.update_data(phone=message.text)\n",[89,758,760],{"class":91,"line":759},20,[89,761,762],{},"    await state.set_state(OrderForm.waiting_description)\n",[89,764,766],{"class":91,"line":765},21,[89,767,768],{},"    await message.answer(\"Describe what you need:\")\n",[89,770,772],{"class":91,"line":771},22,[89,773,197],{"emptyLinePlaceholder":196},[89,775,777],{"class":91,"line":776},23,[89,778,779],{},"@router.message(OrderForm.waiting_description)\n",[89,781,783],{"class":91,"line":782},24,[89,784,785],{},"async def got_description(message: Message, state: FSMContext):\n",[89,787,789],{"class":91,"line":788},25,[89,790,791],{},"    data = await state.get_data()\n",[89,793,795],{"class":91,"line":794},26,[89,796,797],{},"    await state.clear()\n",[89,799,801],{"class":91,"line":800},27,[89,802,803],{},"    await message.answer(\n",[89,805,807],{"class":91,"line":806},28,[89,808,809],{},"        f\"✅ Order received!\\n\"\n",[89,811,813],{"class":91,"line":812},29,[89,814,815],{},"        f\"Name: {data['name']}\\n\"\n",[89,817,819],{"class":91,"line":818},30,[89,820,821],{},"        f\"Phone: {data['phone']}\\n\"\n",[89,823,825],{"class":91,"line":824},31,[89,826,827],{},"        f\"Task: {message.text}\"\n",[89,829,831],{"class":91,"line":830},32,[89,832,833],{},"    )\n",[10,835,395,836,839,840,843,844,847],{},[45,837,838],{},"FSMContext"," is injected automatically by Aiogram. State persists between messages. By default it uses ",[45,841,842],{},"MemoryStorage"," (lost on restart) — for production switch to ",[45,845,846],{},"RedisStorage",".",[14,849,851],{"id":850},"middleware-run-code-on-every-update","Middleware: run code on every update",[80,853,855],{"className":138,"code":854,"language":140,"meta":85,"style":85},"from aiogram import BaseMiddleware\nfrom aiogram.types import TelegramObject\n\nclass LoggingMiddleware(BaseMiddleware):\n    async def __call__(self, handler, event: TelegramObject, data: dict):\n        print(f\"Update from user {event.from_user.id}\")\n        return await handler(event, data)\n\ndp.update.middleware(LoggingMiddleware())\n",[45,856,857,862,867,871,876,881,886,891,895],{"__ignoreMap":85},[89,858,859],{"class":91,"line":92},[89,860,861],{},"from aiogram import BaseMiddleware\n",[89,863,864],{"class":91,"line":150},[89,865,866],{},"from aiogram.types import TelegramObject\n",[89,868,869],{"class":91,"line":175},[89,870,197],{"emptyLinePlaceholder":196},[89,872,873],{"class":91,"line":181},[89,874,875],{},"class LoggingMiddleware(BaseMiddleware):\n",[89,877,878],{"class":91,"line":187},[89,879,880],{},"    async def __call__(self, handler, event: TelegramObject, data: dict):\n",[89,882,883],{"class":91,"line":193},[89,884,885],{},"        print(f\"Update from user {event.from_user.id}\")\n",[89,887,888],{"class":91,"line":200},[89,889,890],{},"        return await handler(event, data)\n",[89,892,893],{"class":91,"line":206},[89,894,197],{"emptyLinePlaceholder":196},[89,896,897],{"class":91,"line":212},[89,898,899],{},"dp.update.middleware(LoggingMiddleware())\n",[10,901,902],{},"I use middleware to auto-save users to the database on first contact — runs before any handler, never forget to add it.",[14,904,906],{"id":905},"deployment-systemd-on-vps","Deployment: systemd on VPS",[10,908,909],{},"The simplest production setup: Ubuntu VPS, systemd service. No Docker needed for simple bots.",[80,911,915],{"className":912,"code":913,"language":914,"meta":85,"style":85},"language-ini shiki shiki-themes github-light github-dark","# \u002Fetc\u002Fsystemd\u002Fsystem\u002Fmybot.service\n[Unit]\nDescription=My Telegram Bot\nAfter=network.target\n\n[Service]\nUser=ubuntu\nWorkingDirectory=\u002Fhome\u002Fubuntu\u002Fmybot\nEnvironment=\"BOT_TOKEN=your_token\"\nExecStart=\u002Fhome\u002Fubuntu\u002Fmybot\u002F.venv\u002Fbin\u002Fpython bot.py\nRestart=always\nRestartSec=5\n\n[Install]\nWantedBy=multi-user.target\n","ini",[45,916,917,922,927,932,937,941,946,951,956,961,966,971,976,980,985],{"__ignoreMap":85},[89,918,919],{"class":91,"line":92},[89,920,921],{},"# \u002Fetc\u002Fsystemd\u002Fsystem\u002Fmybot.service\n",[89,923,924],{"class":91,"line":150},[89,925,926],{},"[Unit]\n",[89,928,929],{"class":91,"line":175},[89,930,931],{},"Description=My Telegram Bot\n",[89,933,934],{"class":91,"line":181},[89,935,936],{},"After=network.target\n",[89,938,939],{"class":91,"line":187},[89,940,197],{"emptyLinePlaceholder":196},[89,942,943],{"class":91,"line":193},[89,944,945],{},"[Service]\n",[89,947,948],{"class":91,"line":200},[89,949,950],{},"User=ubuntu\n",[89,952,953],{"class":91,"line":206},[89,954,955],{},"WorkingDirectory=\u002Fhome\u002Fubuntu\u002Fmybot\n",[89,957,958],{"class":91,"line":212},[89,959,960],{},"Environment=\"BOT_TOKEN=your_token\"\n",[89,962,963],{"class":91,"line":217},[89,964,965],{},"ExecStart=\u002Fhome\u002Fubuntu\u002Fmybot\u002F.venv\u002Fbin\u002Fpython bot.py\n",[89,967,968],{"class":91,"line":223},[89,969,970],{},"Restart=always\n",[89,972,973],{"class":91,"line":229},[89,974,975],{},"RestartSec=5\n",[89,977,978],{"class":91,"line":235},[89,979,197],{"emptyLinePlaceholder":196},[89,981,982],{"class":91,"line":240},[89,983,984],{},"[Install]\n",[89,986,987],{"class":91,"line":246},[89,988,989],{},"WantedBy=multi-user.target\n",[80,991,993],{"className":82,"code":992,"language":84,"meta":85,"style":85},"sudo systemctl enable mybot\nsudo systemctl start mybot\nsudo systemctl status mybot  # check it's running\njournalctl -u mybot -f       # tail the logs\n",[45,994,995,1009,1020,1036],{"__ignoreMap":85},[89,996,997,1000,1003,1006],{"class":91,"line":92},[89,998,999],{"class":95},"sudo",[89,1001,1002],{"class":99}," systemctl",[89,1004,1005],{"class":99}," enable",[89,1007,1008],{"class":99}," mybot\n",[89,1010,1011,1013,1015,1018],{"class":91,"line":150},[89,1012,999],{"class":95},[89,1014,1002],{"class":99},[89,1016,1017],{"class":99}," start",[89,1019,1008],{"class":99},[89,1021,1022,1024,1026,1029,1032],{"class":91,"line":175},[89,1023,999],{"class":95},[89,1025,1002],{"class":99},[89,1027,1028],{"class":99}," status",[89,1030,1031],{"class":99}," mybot",[89,1033,1035],{"class":1034},"sJ8bj","  # check it's running\n",[89,1037,1038,1041,1045,1047,1050],{"class":91,"line":181},[89,1039,1040],{"class":95},"journalctl",[89,1042,1044],{"class":1043},"sj4cs"," -u",[89,1046,1031],{"class":99},[89,1048,1049],{"class":1043}," -f",[89,1051,1052],{"class":1034},"       # tail the logs\n",[10,1054,1055],{},"Your bot now survives VPS reboots and restarts automatically on crashes.",[14,1057,1059],{"id":1058},"what-to-learn-next","What to learn next",[10,1061,1062],{},"Once you're comfortable with the basics:",[22,1064,1065,1071,1085,1091,1097],{},[25,1066,1067,1070],{},[28,1068,1069],{},"PostgreSQL + aiosqlite\u002Fasyncpg"," — most bots need a database eventually",[25,1072,1073,43,1076,48,1079,48,1082],{},[28,1074,1075],{},"Telegram Stars payments",[45,1077,1078],{},"send_invoice()",[45,1080,1081],{},"pre_checkout_query",[45,1083,1084],{},"successful_payment",[25,1086,1087,1090],{},[28,1088,1089],{},"Webhooks"," — faster than polling for high-load bots, requires a public HTTPS domain",[25,1092,1093,1096],{},[28,1094,1095],{},"Telegram Mini App"," — build a full web UI that opens inside Telegram",[25,1098,1099,1102],{},[28,1100,1101],{},"Broadcast system"," — send messages to all users without hitting rate limits",[10,1104,1105,1106,1111,1112,1117],{},"The documentation at ",[108,1107,1110],{"href":1108,"rel":1109},"https:\u002F\u002Fdocs.aiogram.dev",[112],"docs.aiogram.dev"," is solid. The Aiogram Telegram community (",[108,1113,1116],{"href":1114,"rel":1115},"https:\u002F\u002Ft.me\u002Faiogram",[112],"@aiogram",") is active and helpful.",[10,1119,1120],{},"Build something real, deploy it, watch it run. That's how you actually learn this.",[1122,1123,1124],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html pre.shiki code .sScJk, html code.shiki .sScJk{--shiki-default:#6F42C1;--shiki-dark:#B392F0}html pre.shiki code .sZZnC, html code.shiki .sZZnC{--shiki-default:#032F62;--shiki-dark:#9ECBFF}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":85,"searchDepth":150,"depth":150,"links":1126},[1127,1128,1129,1130,1131,1132,1133,1134,1135,1136],{"id":16,"depth":150,"text":17},{"id":77,"depth":150,"text":78},{"id":156,"depth":150,"text":157},{"id":276,"depth":150,"text":277},{"id":391,"depth":150,"text":392},{"id":489,"depth":150,"text":490},{"id":615,"depth":150,"text":616},{"id":850,"depth":150,"text":851},{"id":905,"depth":150,"text":906},{"id":1058,"depth":150,"text":1059},"2025-04-10","Step-by-step guide to building a Telegram bot with Aiogram 3 — handlers, FSM, inline keyboards, and deployment. No filler.","md",{},"\u002Fen\u002Fblog\u002Faiogram-3-tutorial-2025","12 min",{"title":5,"description":1138},"aiogram-3-tutorial-2025","en\u002Fblog\u002Faiogram-3-tutorial-2025",[1147,1148,1149,1150,1151],"Aiogram","Python","Tutorial","Telegram","Bot","VC3IAPwxHKb8OuEGjjDqb0NBVXCPktyK7Tt91sATYVY",1783440106482]