[{"data":1,"prerenderedAt":2143},["ShallowReactive",2],{"telegram-bots-guide-posts":3},[4,879,1969],{"id":5,"title":6,"body":7,"cluster":855,"ctaType":856,"date":857,"description":858,"extension":859,"intent":860,"meta":861,"navigation":179,"ogImage":862,"path":863,"primaryKeyword":864,"readTime":865,"relatedCases":866,"relatedCourse":868,"relatedService":869,"seo":870,"slug":871,"stem":872,"tags":873,"updated":877,"__hash__":878},"blogRu\u002Fru\u002Fblog\u002Ftelegram-bot-python-2024.md","Как написать Telegram-бота на Python и Aiogram 3 — полное руководство",{"type":8,"value":9,"toc":842},"minimark",[10,19,33,40,45,78,82,85,99,103,141,144,152,156,233,237,334,338,406,410,502,506,578,582,626,763,767,820,824,831,838],[11,12,13,14,18],"p",{},"Telegram-боты — один из самых быстрых способов автоматизировать бизнес-процессы или создать полноценный продукт. В этой статье я расскажу, как с нуля построить ",[15,16,17],"strong",{},"production-ready бота"," с базой данных, правильной архитектурой и деплоем.",[11,20,21,22,27,28,32],{},"Для первого знакомства можно пройти ",[23,24,26],"a",{"href":25},"\u002Fru\u002Fcourses\u002Faiogram-start\u002F","бесплатный практикум по Aiogram 3"," на сайте, в Mini App или в боте, а для коммерческих сценариев с платежами и Mini App — ",[23,29,31],{"href":30},"\u002Fru\u002Fcourses\u002Ftelegram-bots-python\u002F","полный курс по Telegram-ботам",".",[11,34,35],{},[36,37],"img",{"alt":38,"src":39},"Архитектура Telegram-бота на Aiogram 3","\u002Fimages\u002Fblog\u002Fbot-architecture.svg",[41,42,44],"h2",{"id":43},"что-мы-построим","Что мы построим",[46,47,48,56,62,69,75],"ul",{},[49,50,51,52,55],"li",{},"Бот на ",[15,53,54],{},"Aiogram 3"," (async, современный API)",[49,57,58,61],{},[15,59,60],{},"PostgreSQL"," как хранилище данных",[49,63,64,65],{},"Конфигурация через ",[66,67,68],"code",{},".env",[49,70,71,74],{},[15,72,73],{},"Docker"," для деплоя",[49,76,77],{},"FSM (конечный автомат состояний) для многошаговых сценариев",[41,79,81],{"id":80},"почему-aiogram-3-а-не-python-telegram-bot","Почему Aiogram 3, а не python-telegram-bot",[11,83,84],{},"Aiogram 3 — де-факто стандарт для серьёзных ботов на Python:",[46,86,87,90,93,96],{},[49,88,89],{},"Полностью асинхронный (asyncio)",[49,91,92],{},"Роутеры и мидлвары как в FastAPI",[49,94,95],{},"Удобный FSM для диалогов",[49,97,98],{},"Активная поддержка и хорошая документация",[41,100,102],{"id":101},"установка-и-структура-проекта","Установка и структура проекта",[104,105,110],"pre",{"className":106,"code":107,"language":108,"meta":109,"style":109},"language-bash shiki shiki-themes github-light github-dark","pip install aiogram==3.* sqlalchemy asyncpg python-dotenv\n","bash","",[66,111,112],{"__ignoreMap":109},[113,114,117,121,125,128,132,135,138],"span",{"class":115,"line":116},"line",1,[113,118,120],{"class":119},"sScJk","pip",[113,122,124],{"class":123},"sZZnC"," install",[113,126,127],{"class":123}," aiogram==3.",[113,129,131],{"class":130},"sj4cs","*",[113,133,134],{"class":123}," sqlalchemy",[113,136,137],{"class":123}," asyncpg",[113,139,140],{"class":123}," python-dotenv\n",[11,142,143],{},"Структура проекта:",[104,145,150],{"className":146,"code":148,"language":149},[147],"language-text","bot\u002F\n├── handlers\u002F\n│   ├── __init__.py\n│   ├── common.py      # \u002Fstart, \u002Fhelp\n│   └── user.py        # пользовательские сценарии\n├── middlewares\u002F\n│   └── database.py    # инжект сессии в хэндлеры\n├── models\u002F\n│   └── user.py        # SQLAlchemy модели\n├── keyboards\u002F\n│   └── reply.py\n├── config.py\n└── main.py\n","text",[66,151,148],{"__ignoreMap":109},[41,153,155],{"id":154},"конфигурация","Конфигурация",[104,157,161],{"className":158,"code":159,"language":160,"meta":109,"style":109},"language-python shiki shiki-themes github-light github-dark","# config.py\nfrom pydantic_settings import BaseSettings\n\nclass Settings(BaseSettings):\n    BOT_TOKEN: str\n    DATABASE_URL: str\n    ADMIN_ID: int\n\n    class Config:\n        env_file = \".env\"\n\nsettings = Settings()\n","python",[66,162,163,168,174,181,187,193,199,205,210,216,222,227],{"__ignoreMap":109},[113,164,165],{"class":115,"line":116},[113,166,167],{},"# config.py\n",[113,169,171],{"class":115,"line":170},2,[113,172,173],{},"from pydantic_settings import BaseSettings\n",[113,175,177],{"class":115,"line":176},3,[113,178,180],{"emptyLinePlaceholder":179},true,"\n",[113,182,184],{"class":115,"line":183},4,[113,185,186],{},"class Settings(BaseSettings):\n",[113,188,190],{"class":115,"line":189},5,[113,191,192],{},"    BOT_TOKEN: str\n",[113,194,196],{"class":115,"line":195},6,[113,197,198],{},"    DATABASE_URL: str\n",[113,200,202],{"class":115,"line":201},7,[113,203,204],{},"    ADMIN_ID: int\n",[113,206,208],{"class":115,"line":207},8,[113,209,180],{"emptyLinePlaceholder":179},[113,211,213],{"class":115,"line":212},9,[113,214,215],{},"    class Config:\n",[113,217,219],{"class":115,"line":218},10,[113,220,221],{},"        env_file = \".env\"\n",[113,223,225],{"class":115,"line":224},11,[113,226,180],{"emptyLinePlaceholder":179},[113,228,230],{"class":115,"line":229},12,[113,231,232],{},"settings = Settings()\n",[41,234,236],{"id":235},"основной-файл","Основной файл",[104,238,240],{"className":158,"code":239,"language":160,"meta":109,"style":109},"# main.py\nimport asyncio\nfrom aiogram import Bot, Dispatcher\nfrom aiogram.fsm.storage.memory import MemoryStorage\nfrom handlers import common, user\nfrom config import settings\n\nasync def main():\n    bot = Bot(token=settings.BOT_TOKEN)\n    dp = Dispatcher(storage=MemoryStorage())\n\n    dp.include_router(common.router)\n    dp.include_router(user.router)\n\n    await dp.start_polling(bot)\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n",[66,241,242,247,252,257,262,267,272,276,281,286,291,295,300,306,311,317,322,328],{"__ignoreMap":109},[113,243,244],{"class":115,"line":116},[113,245,246],{},"# main.py\n",[113,248,249],{"class":115,"line":170},[113,250,251],{},"import asyncio\n",[113,253,254],{"class":115,"line":176},[113,255,256],{},"from aiogram import Bot, Dispatcher\n",[113,258,259],{"class":115,"line":183},[113,260,261],{},"from aiogram.fsm.storage.memory import MemoryStorage\n",[113,263,264],{"class":115,"line":189},[113,265,266],{},"from handlers import common, user\n",[113,268,269],{"class":115,"line":195},[113,270,271],{},"from config import settings\n",[113,273,274],{"class":115,"line":201},[113,275,180],{"emptyLinePlaceholder":179},[113,277,278],{"class":115,"line":207},[113,279,280],{},"async def main():\n",[113,282,283],{"class":115,"line":212},[113,284,285],{},"    bot = Bot(token=settings.BOT_TOKEN)\n",[113,287,288],{"class":115,"line":218},[113,289,290],{},"    dp = Dispatcher(storage=MemoryStorage())\n",[113,292,293],{"class":115,"line":224},[113,294,180],{"emptyLinePlaceholder":179},[113,296,297],{"class":115,"line":229},[113,298,299],{},"    dp.include_router(common.router)\n",[113,301,303],{"class":115,"line":302},13,[113,304,305],{},"    dp.include_router(user.router)\n",[113,307,309],{"class":115,"line":308},14,[113,310,180],{"emptyLinePlaceholder":179},[113,312,314],{"class":115,"line":313},15,[113,315,316],{},"    await dp.start_polling(bot)\n",[113,318,320],{"class":115,"line":319},16,[113,321,180],{"emptyLinePlaceholder":179},[113,323,325],{"class":115,"line":324},17,[113,326,327],{},"if __name__ == \"__main__\":\n",[113,329,331],{"class":115,"line":330},18,[113,332,333],{},"    asyncio.run(main())\n",[41,335,337],{"id":336},"хэндлер-start","Хэндлер \u002Fstart",[104,339,341],{"className":158,"code":340,"language":160,"meta":109,"style":109},"# handlers\u002Fcommon.py\nfrom aiogram import Router\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(\n        f\"Привет, {message.from_user.first_name}! 👋\\n\\n\"\n        \"Я помогу тебе автоматизировать твои задачи.\"\n    )\n",[66,342,343,348,353,358,363,367,372,376,381,386,391,396,401],{"__ignoreMap":109},[113,344,345],{"class":115,"line":116},[113,346,347],{},"# handlers\u002Fcommon.py\n",[113,349,350],{"class":115,"line":170},[113,351,352],{},"from aiogram import Router\n",[113,354,355],{"class":115,"line":176},[113,356,357],{},"from aiogram.filters import CommandStart\n",[113,359,360],{"class":115,"line":183},[113,361,362],{},"from aiogram.types import Message\n",[113,364,365],{"class":115,"line":189},[113,366,180],{"emptyLinePlaceholder":179},[113,368,369],{"class":115,"line":195},[113,370,371],{},"router = Router()\n",[113,373,374],{"class":115,"line":201},[113,375,180],{"emptyLinePlaceholder":179},[113,377,378],{"class":115,"line":207},[113,379,380],{},"@router.message(CommandStart())\n",[113,382,383],{"class":115,"line":212},[113,384,385],{},"async def cmd_start(message: Message):\n",[113,387,388],{"class":115,"line":218},[113,389,390],{},"    await message.answer(\n",[113,392,393],{"class":115,"line":224},[113,394,395],{},"        f\"Привет, {message.from_user.first_name}! 👋\\n\\n\"\n",[113,397,398],{"class":115,"line":229},[113,399,400],{},"        \"Я помогу тебе автоматизировать твои задачи.\"\n",[113,402,403],{"class":115,"line":302},[113,404,405],{},"    )\n",[41,407,409],{"id":408},"fsm-многошаговые-диалоги","FSM — многошаговые диалоги",[104,411,413],{"className":158,"code":412,"language":160,"meta":109,"style":109},"from aiogram.fsm.context import FSMContext\nfrom aiogram.fsm.state import State, StatesGroup\n\nclass OrderForm(StatesGroup):\n    waiting_name    = State()\n    waiting_phone   = State()\n    waiting_comment = State()\n\n@router.message(Command(\"order\"))\nasync def start_order(message: Message, state: FSMContext):\n    await state.set_state(OrderForm.waiting_name)\n    await message.answer(\"Как вас зовут?\")\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(\"Укажите номер телефона:\")\n",[66,414,415,420,425,429,434,439,444,449,453,458,463,468,473,477,482,487,492,497],{"__ignoreMap":109},[113,416,417],{"class":115,"line":116},[113,418,419],{},"from aiogram.fsm.context import FSMContext\n",[113,421,422],{"class":115,"line":170},[113,423,424],{},"from aiogram.fsm.state import State, StatesGroup\n",[113,426,427],{"class":115,"line":176},[113,428,180],{"emptyLinePlaceholder":179},[113,430,431],{"class":115,"line":183},[113,432,433],{},"class OrderForm(StatesGroup):\n",[113,435,436],{"class":115,"line":189},[113,437,438],{},"    waiting_name    = State()\n",[113,440,441],{"class":115,"line":195},[113,442,443],{},"    waiting_phone   = State()\n",[113,445,446],{"class":115,"line":201},[113,447,448],{},"    waiting_comment = State()\n",[113,450,451],{"class":115,"line":207},[113,452,180],{"emptyLinePlaceholder":179},[113,454,455],{"class":115,"line":212},[113,456,457],{},"@router.message(Command(\"order\"))\n",[113,459,460],{"class":115,"line":218},[113,461,462],{},"async def start_order(message: Message, state: FSMContext):\n",[113,464,465],{"class":115,"line":224},[113,466,467],{},"    await state.set_state(OrderForm.waiting_name)\n",[113,469,470],{"class":115,"line":229},[113,471,472],{},"    await message.answer(\"Как вас зовут?\")\n",[113,474,475],{"class":115,"line":302},[113,476,180],{"emptyLinePlaceholder":179},[113,478,479],{"class":115,"line":308},[113,480,481],{},"@router.message(OrderForm.waiting_name)\n",[113,483,484],{"class":115,"line":313},[113,485,486],{},"async def got_name(message: Message, state: FSMContext):\n",[113,488,489],{"class":115,"line":319},[113,490,491],{},"    await state.update_data(name=message.text)\n",[113,493,494],{"class":115,"line":324},[113,495,496],{},"    await state.set_state(OrderForm.waiting_phone)\n",[113,498,499],{"class":115,"line":330},[113,500,501],{},"    await message.answer(\"Укажите номер телефона:\")\n",[41,503,505],{"id":504},"работа-с-базой-данных","Работа с базой данных",[104,507,509],{"className":158,"code":508,"language":160,"meta":109,"style":109},"# models\u002Fuser.py\nfrom sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column\nfrom sqlalchemy import BigInteger, String, DateTime, func\n\nclass Base(DeclarativeBase):\n    pass\n\nclass User(Base):\n    __tablename__ = \"users\"\n\n    id:         Mapped[int]    = mapped_column(BigInteger, primary_key=True)\n    username:   Mapped[str | None] = mapped_column(String(64))\n    first_name: Mapped[str]    = mapped_column(String(128))\n    created_at: Mapped[DateTime] = mapped_column(server_default=func.now())\n",[66,510,511,516,521,526,530,535,540,544,549,554,558,563,568,573],{"__ignoreMap":109},[113,512,513],{"class":115,"line":116},[113,514,515],{},"# models\u002Fuser.py\n",[113,517,518],{"class":115,"line":170},[113,519,520],{},"from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column\n",[113,522,523],{"class":115,"line":176},[113,524,525],{},"from sqlalchemy import BigInteger, String, DateTime, func\n",[113,527,528],{"class":115,"line":183},[113,529,180],{"emptyLinePlaceholder":179},[113,531,532],{"class":115,"line":189},[113,533,534],{},"class Base(DeclarativeBase):\n",[113,536,537],{"class":115,"line":195},[113,538,539],{},"    pass\n",[113,541,542],{"class":115,"line":201},[113,543,180],{"emptyLinePlaceholder":179},[113,545,546],{"class":115,"line":207},[113,547,548],{},"class User(Base):\n",[113,550,551],{"class":115,"line":212},[113,552,553],{},"    __tablename__ = \"users\"\n",[113,555,556],{"class":115,"line":218},[113,557,180],{"emptyLinePlaceholder":179},[113,559,560],{"class":115,"line":224},[113,561,562],{},"    id:         Mapped[int]    = mapped_column(BigInteger, primary_key=True)\n",[113,564,565],{"class":115,"line":229},[113,566,567],{},"    username:   Mapped[str | None] = mapped_column(String(64))\n",[113,569,570],{"class":115,"line":302},[113,571,572],{},"    first_name: Mapped[str]    = mapped_column(String(128))\n",[113,574,575],{"class":115,"line":308},[113,576,577],{},"    created_at: Mapped[DateTime] = mapped_column(server_default=func.now())\n",[41,579,581],{"id":580},"dockerfile","Dockerfile",[104,583,586],{"className":584,"code":585,"language":580,"meta":109,"style":109},"language-dockerfile shiki shiki-themes github-light github-dark","FROM python:3.12-slim\n\nWORKDIR \u002Fapp\nCOPY requirements.txt .\nRUN pip install --no-cache-dir -r requirements.txt\n\nCOPY . .\nCMD [\"python\", \"main.py\"]\n",[66,587,588,593,597,602,607,612,616,621],{"__ignoreMap":109},[113,589,590],{"class":115,"line":116},[113,591,592],{},"FROM python:3.12-slim\n",[113,594,595],{"class":115,"line":170},[113,596,180],{"emptyLinePlaceholder":179},[113,598,599],{"class":115,"line":176},[113,600,601],{},"WORKDIR \u002Fapp\n",[113,603,604],{"class":115,"line":183},[113,605,606],{},"COPY requirements.txt .\n",[113,608,609],{"class":115,"line":189},[113,610,611],{},"RUN pip install --no-cache-dir -r requirements.txt\n",[113,613,614],{"class":115,"line":195},[113,615,180],{"emptyLinePlaceholder":179},[113,617,618],{"class":115,"line":201},[113,619,620],{},"COPY . .\n",[113,622,623],{"class":115,"line":207},[113,624,625],{},"CMD [\"python\", \"main.py\"]\n",[104,627,631],{"className":628,"code":629,"language":630,"meta":109,"style":109},"language-yaml shiki shiki-themes github-light github-dark","# docker-compose.yml\nservices:\n  bot:\n    build: .\n    env_file: .env\n    depends_on: [db]\n    restart: unless-stopped\n\n  db:\n    image: postgres:16-alpine\n    env_file: .env\n    volumes:\n      - pgdata:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata\n\nvolumes:\n  pgdata:\n","yaml",[66,632,633,639,649,656,667,677,691,701,705,712,722,730,737,745,749,756],{"__ignoreMap":109},[113,634,635],{"class":115,"line":116},[113,636,638],{"class":637},"sJ8bj","# docker-compose.yml\n",[113,640,641,645],{"class":115,"line":170},[113,642,644],{"class":643},"s9eBZ","services",[113,646,648],{"class":647},"sVt8B",":\n",[113,650,651,654],{"class":115,"line":176},[113,652,653],{"class":643},"  bot",[113,655,648],{"class":647},[113,657,658,661,664],{"class":115,"line":183},[113,659,660],{"class":643},"    build",[113,662,663],{"class":647},": ",[113,665,666],{"class":130},".\n",[113,668,669,672,674],{"class":115,"line":189},[113,670,671],{"class":643},"    env_file",[113,673,663],{"class":647},[113,675,676],{"class":123},".env\n",[113,678,679,682,685,688],{"class":115,"line":195},[113,680,681],{"class":643},"    depends_on",[113,683,684],{"class":647},": [",[113,686,687],{"class":123},"db",[113,689,690],{"class":647},"]\n",[113,692,693,696,698],{"class":115,"line":201},[113,694,695],{"class":643},"    restart",[113,697,663],{"class":647},[113,699,700],{"class":123},"unless-stopped\n",[113,702,703],{"class":115,"line":207},[113,704,180],{"emptyLinePlaceholder":179},[113,706,707,710],{"class":115,"line":212},[113,708,709],{"class":643},"  db",[113,711,648],{"class":647},[113,713,714,717,719],{"class":115,"line":218},[113,715,716],{"class":643},"    image",[113,718,663],{"class":647},[113,720,721],{"class":123},"postgres:16-alpine\n",[113,723,724,726,728],{"class":115,"line":224},[113,725,671],{"class":643},[113,727,663],{"class":647},[113,729,676],{"class":123},[113,731,732,735],{"class":115,"line":229},[113,733,734],{"class":643},"    volumes",[113,736,648],{"class":647},[113,738,739,742],{"class":115,"line":302},[113,740,741],{"class":647},"      - ",[113,743,744],{"class":123},"pgdata:\u002Fvar\u002Flib\u002Fpostgresql\u002Fdata\n",[113,746,747],{"class":115,"line":308},[113,748,180],{"emptyLinePlaceholder":179},[113,750,751,754],{"class":115,"line":313},[113,752,753],{"class":643},"volumes",[113,755,648],{"class":647},[113,757,758,761],{"class":115,"line":319},[113,759,760],{"class":643},"  pgdata",[113,762,648],{"class":647},[41,764,766],{"id":765},"деплой-на-vps","Деплой на VPS",[104,768,770],{"className":106,"code":769,"language":108,"meta":109,"style":109},"git clone your-repo && cd bot\ncp .env.example .env  # заполнить токены\ndocker compose up -d\n",[66,771,772,792,806],{"__ignoreMap":109},[113,773,774,777,780,783,786,789],{"class":115,"line":116},[113,775,776],{"class":119},"git",[113,778,779],{"class":123}," clone",[113,781,782],{"class":123}," your-repo",[113,784,785],{"class":647}," && ",[113,787,788],{"class":130},"cd",[113,790,791],{"class":123}," bot\n",[113,793,794,797,800,803],{"class":115,"line":170},[113,795,796],{"class":119},"cp",[113,798,799],{"class":123}," .env.example",[113,801,802],{"class":123}," .env",[113,804,805],{"class":637},"  # заполнить токены\n",[113,807,808,811,814,817],{"class":115,"line":176},[113,809,810],{"class":119},"docker",[113,812,813],{"class":123}," compose",[113,815,816],{"class":123}," up",[113,818,819],{"class":130}," -d\n",[41,821,823],{"id":822},"итог","Итог",[11,825,826,827,830],{},"Правильно выстроенный Telegram-бот — это не просто скрипт с ",[66,828,829],{},"if '\u002Fstart'",". Это полноценное приложение с роутерами, мидлварами, базой данных и CI\u002FCD. Aiogram 3 даёт для этого все инструменты.",[11,832,833,834,32],{},"Если нужна помощь с разработкой бота — ",[23,835,837],{"href":836},"\u002Fru#contact","напишите мне",[839,840,841],"style",{},"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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}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 .sVt8B, html code.shiki .sVt8B{--shiki-default:#24292E;--shiki-dark:#E1E4E8}html pre.shiki code .sJ8bj, html code.shiki .sJ8bj{--shiki-default:#6A737D;--shiki-dark:#6A737D}html pre.shiki code .s9eBZ, html code.shiki .s9eBZ{--shiki-default:#22863A;--shiki-dark:#85E89D}",{"title":109,"searchDepth":170,"depth":170,"links":843},[844,845,846,847,848,849,850,851,852,853,854],{"id":43,"depth":170,"text":44},{"id":80,"depth":170,"text":81},{"id":101,"depth":170,"text":102},{"id":154,"depth":170,"text":155},{"id":235,"depth":170,"text":236},{"id":336,"depth":170,"text":337},{"id":408,"depth":170,"text":409},{"id":504,"depth":170,"text":505},{"id":580,"depth":170,"text":581},{"id":765,"depth":170,"text":766},{"id":822,"depth":170,"text":823},"telegram-bots","course","2024-11-15","Пошаговое руководство по созданию production-ready Telegram-бота с использованием Aiogram 3, PostgreSQL и деплоем на VPS. От нуля до рабочего продукта.","md","educational",{},"\u002Fog-image-v2.png","\u002Fru\u002Fblog\u002Ftelegram-bot-python-2024","как написать Telegram-бота на Python","12 мин",[867],"fazer-cards-bot","aiogram-start","bots",{"title":6,"description":858},"telegram-bot-python-2024","ru\u002Fblog\u002Ftelegram-bot-python-2024",[874,875,876,60,73],"Python","Telegram","Aiogram","2026-08-28","CslVHfrpOqkN98xLR4UxRSsKKHATRj8MTMnh15fShiQ",{"id":880,"title":881,"body":882,"cluster":855,"ctaType":856,"date":1956,"description":1957,"extension":859,"intent":860,"meta":1958,"navigation":179,"ogImage":862,"path":1959,"primaryKeyword":1960,"readTime":865,"relatedCases":1961,"relatedCourse":868,"relatedService":869,"seo":1962,"slug":1963,"stem":1964,"tags":1965,"updated":877,"__hash__":1968},"blogRu\u002Fru\u002Fblog\u002Faiogram-3-tutorial-2025.md","Aiogram 3: туториал по созданию Telegram-бота на Python (2026)",{"type":8,"value":883,"toc":1943},[884,887,894,898,901,944,951,955,969,983,986,992,998,1013,1017,1099,1106,1110,1121,1188,1216,1219,1223,1230,1310,1317,1321,1380,1436,1443,1447,1453,1456,1459,1496,1662,1675,1679,1727,1730,1734,1737,1817,1872,1875,1879,1882,1922,1937,1940],[11,885,886],{},"Aiogram 3 — это асинхронный Python-фреймворк для Telegram-ботов. Я использовал его в 20+ продакшн-проектах. Вот всё, что нужно чтобы пройти путь от нуля до запущенного бота, не продираясь через устаревшие туториалы.",[11,888,889,890,893],{},"Если удобнее идти по шагам с готовым проектом, начни с ",[23,891,892],{"href":25},"бесплатного практикума Aiogram 3",". Там шесть уроков, FSM, inline-клавиатуры и деплой на VPS; проходить их можно на сайте, в Mini App или в боте.",[41,895,897],{"id":896},"почему-именно-aiogram-3","Почему именно Aiogram 3",[11,899,900],{},"Есть несколько Python-библиотек для Telegram-ботов. Aiogram 3 выигрывает для продакшна потому что:",[46,902,903,909,915,932,938],{},[49,904,905,908],{},[15,906,907],{},"Async-first"," — построен на asyncio, обрабатывает тысячи обновлений эффективно",[49,910,911,914],{},[15,912,913],{},"Система роутеров"," — разделяйте хендлеры по модулям чисто и аккуратно",[49,916,917,920,921,924,925,924,928,931],{},[15,918,919],{},"Magic filters"," — ",[66,922,923],{},"F.text == \"привет\"",", ",[66,926,927],{},"F.photo",[66,929,930],{},"F.from_user.id == 123456"," — выразительно и читаемо",[49,933,934,937],{},[15,935,936],{},"FSM"," — встроенный конечный автомат для многошаговых диалогов",[49,939,940,943],{},[15,941,942],{},"Типобезопасность"," — полная типизация во всём фреймворке",[11,945,946,947,950],{},"Старый ",[66,948,949],{},"python-telegram-bot"," тоже работает, но архитектура Aiogram 3 чище для любого проекта сложнее игрушечного.",[41,952,954],{"id":953},"установка","Установка",[104,956,958],{"className":106,"code":957,"language":108,"meta":109,"style":109},"pip install aiogram\n",[66,959,960],{"__ignoreMap":109},[113,961,962,964,966],{"class":115,"line":116},[113,963,120],{"class":119},[113,965,124],{"class":123},[113,967,968],{"class":123}," aiogram\n",[11,970,971,972,978,979,982],{},"Получите токен бота у ",[23,973,977],{"href":974,"rel":975},"https:\u002F\u002Ft.me\u002FBotFather",[976],"nofollow","@BotFather"," — отправьте ",[66,980,981],{},"\u002Fnewbot",", следуйте инструкциям.",[11,984,985],{},"Структура проекта, которую я использую:",[104,987,990],{"className":988,"code":989,"language":149},[147],"mybot\u002F\n├── bot.py          # точка входа\n├── config.py       # токен + настройки\n├── handlers\u002F\n│   ├── start.py\n│   └── orders.py\n├── keyboards\u002F\n│   └── kb.py\n└── states\u002F\n    └── states.py\n",[66,991,989],{"__ignoreMap":109},[11,993,994,997],{},[66,995,996],{},"config.py",":",[104,999,1001],{"className":158,"code":1000,"language":160,"meta":109,"style":109},"import os\nBOT_TOKEN = os.getenv(\"BOT_TOKEN\", \"your_token_here\")\n",[66,1002,1003,1008],{"__ignoreMap":109},[113,1004,1005],{"class":115,"line":116},[113,1006,1007],{},"import os\n",[113,1009,1010],{"class":115,"line":170},[113,1011,1012],{},"BOT_TOKEN = os.getenv(\"BOT_TOKEN\", \"your_token_here\")\n",[41,1014,1016],{"id":1015},"первый-бот-хендлер-start","Первый бот: хендлер \u002Fstart",[104,1018,1020],{"className":158,"code":1019,"language":160,"meta":109,"style":109},"# 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\"Привет, {message.from_user.first_name}!\")\n\nasync def main():\n    await dp.start_polling(bot)\n\nif __name__ == \"__main__\":\n    asyncio.run(main())\n",[66,1021,1022,1027,1031,1035,1039,1043,1047,1052,1057,1061,1066,1070,1075,1079,1083,1087,1091,1095],{"__ignoreMap":109},[113,1023,1024],{"class":115,"line":116},[113,1025,1026],{},"# bot.py\n",[113,1028,1029],{"class":115,"line":170},[113,1030,251],{},[113,1032,1033],{"class":115,"line":176},[113,1034,256],{},[113,1036,1037],{"class":115,"line":183},[113,1038,357],{},[113,1040,1041],{"class":115,"line":189},[113,1042,362],{},[113,1044,1045],{"class":115,"line":195},[113,1046,180],{"emptyLinePlaceholder":179},[113,1048,1049],{"class":115,"line":201},[113,1050,1051],{},"bot = Bot(token=\"YOUR_TOKEN\")\n",[113,1053,1054],{"class":115,"line":207},[113,1055,1056],{},"dp = Dispatcher()\n",[113,1058,1059],{"class":115,"line":212},[113,1060,180],{"emptyLinePlaceholder":179},[113,1062,1063],{"class":115,"line":218},[113,1064,1065],{},"@dp.message(CommandStart())\n",[113,1067,1068],{"class":115,"line":224},[113,1069,385],{},[113,1071,1072],{"class":115,"line":229},[113,1073,1074],{},"    await message.answer(f\"Привет, {message.from_user.first_name}!\")\n",[113,1076,1077],{"class":115,"line":302},[113,1078,180],{"emptyLinePlaceholder":179},[113,1080,1081],{"class":115,"line":308},[113,1082,280],{},[113,1084,1085],{"class":115,"line":313},[113,1086,316],{},[113,1088,1089],{"class":115,"line":319},[113,1090,180],{"emptyLinePlaceholder":179},[113,1092,1093],{"class":115,"line":324},[113,1094,327],{},[113,1096,1097],{"class":115,"line":330},[113,1098,333],{},[11,1100,1101,1102,1105],{},"Запустите, отправьте ",[66,1103,1104],{},"\u002Fstart"," своему боту. Это база.",[41,1107,1109],{"id":1108},"роутеры-держим-код-в-порядке","Роутеры: держим код в порядке",[11,1111,1112,1113,1116,1117,1120],{},"В Aiogram 3 ",[66,1114,1115],{},"Router"," — ключевая абстракция. Вместо того чтобы регистрировать всё на ",[66,1118,1119],{},"Dispatcher",", создавайте роутеры на каждый модуль и подключайте их:",[104,1122,1124],{"className":158,"code":1123,"language":160,"meta":109,"style":109},"# 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(\"Добро пожаловать!\")\n\n@router.message(F.text == \"помощь\")\nasync def help_text(message: Message):\n    await message.answer(\"Вот как работает этот бот...\")\n",[66,1125,1126,1131,1136,1140,1144,1148,1152,1156,1160,1164,1169,1173,1178,1183],{"__ignoreMap":109},[113,1127,1128],{"class":115,"line":116},[113,1129,1130],{},"# handlers\u002Fstart.py\n",[113,1132,1133],{"class":115,"line":170},[113,1134,1135],{},"from aiogram import Router, F\n",[113,1137,1138],{"class":115,"line":176},[113,1139,357],{},[113,1141,1142],{"class":115,"line":183},[113,1143,362],{},[113,1145,1146],{"class":115,"line":189},[113,1147,180],{"emptyLinePlaceholder":179},[113,1149,1150],{"class":115,"line":195},[113,1151,371],{},[113,1153,1154],{"class":115,"line":201},[113,1155,180],{"emptyLinePlaceholder":179},[113,1157,1158],{"class":115,"line":207},[113,1159,380],{},[113,1161,1162],{"class":115,"line":212},[113,1163,385],{},[113,1165,1166],{"class":115,"line":218},[113,1167,1168],{},"    await message.answer(\"Добро пожаловать!\")\n",[113,1170,1171],{"class":115,"line":224},[113,1172,180],{"emptyLinePlaceholder":179},[113,1174,1175],{"class":115,"line":229},[113,1176,1177],{},"@router.message(F.text == \"помощь\")\n",[113,1179,1180],{"class":115,"line":302},[113,1181,1182],{},"async def help_text(message: Message):\n",[113,1184,1185],{"class":115,"line":308},[113,1186,1187],{},"    await message.answer(\"Вот как работает этот бот...\")\n",[104,1189,1191],{"className":158,"code":1190,"language":160,"meta":109,"style":109},"# bot.py\nfrom handlers import start, orders\n\ndp.include_router(start.router)\ndp.include_router(orders.router)\n",[66,1192,1193,1197,1202,1206,1211],{"__ignoreMap":109},[113,1194,1195],{"class":115,"line":116},[113,1196,1026],{},[113,1198,1199],{"class":115,"line":170},[113,1200,1201],{},"from handlers import start, orders\n",[113,1203,1204],{"class":115,"line":176},[113,1205,180],{"emptyLinePlaceholder":179},[113,1207,1208],{"class":115,"line":183},[113,1209,1210],{},"dp.include_router(start.router)\n",[113,1212,1213],{"class":115,"line":189},[113,1214,1215],{},"dp.include_router(orders.router)\n",[11,1217,1218],{},"Это сохраняет больших ботов поддерживаемыми. Каждый файл хендлеров остаётся сфокусированным.",[41,1220,1222],{"id":1221},"magic-filters-f","Magic filters: F.",[11,1224,1225,1226,1229],{},"Объект ",[66,1227,1228],{},"F"," — то, где Aiogram 3 особенно хорош:",[104,1231,1233],{"className":158,"code":1232,"language":160,"meta":109,"style":109},"from aiogram import F\n\n# Точное совпадение текста\n@router.message(F.text == \"Купить\")\n\n# Сообщения с фото\n@router.message(F.photo)\n\n# Конкретный пользователь\n@router.message(F.from_user.id == 123456789)\n\n# Комбинирование фильтров\n@router.message(F.text.startswith(\"заказ:\") & F.from_user.is_premium)\n\n# Отрицание\n@router.message(~F.text.in_({\"стоп\", \"отмена\"}))\n",[66,1234,1235,1240,1244,1249,1254,1258,1263,1268,1272,1277,1282,1286,1291,1296,1300,1305],{"__ignoreMap":109},[113,1236,1237],{"class":115,"line":116},[113,1238,1239],{},"from aiogram import F\n",[113,1241,1242],{"class":115,"line":170},[113,1243,180],{"emptyLinePlaceholder":179},[113,1245,1246],{"class":115,"line":176},[113,1247,1248],{},"# Точное совпадение текста\n",[113,1250,1251],{"class":115,"line":183},[113,1252,1253],{},"@router.message(F.text == \"Купить\")\n",[113,1255,1256],{"class":115,"line":189},[113,1257,180],{"emptyLinePlaceholder":179},[113,1259,1260],{"class":115,"line":195},[113,1261,1262],{},"# Сообщения с фото\n",[113,1264,1265],{"class":115,"line":201},[113,1266,1267],{},"@router.message(F.photo)\n",[113,1269,1270],{"class":115,"line":207},[113,1271,180],{"emptyLinePlaceholder":179},[113,1273,1274],{"class":115,"line":212},[113,1275,1276],{},"# Конкретный пользователь\n",[113,1278,1279],{"class":115,"line":218},[113,1280,1281],{},"@router.message(F.from_user.id == 123456789)\n",[113,1283,1284],{"class":115,"line":224},[113,1285,180],{"emptyLinePlaceholder":179},[113,1287,1288],{"class":115,"line":229},[113,1289,1290],{},"# Комбинирование фильтров\n",[113,1292,1293],{"class":115,"line":302},[113,1294,1295],{},"@router.message(F.text.startswith(\"заказ:\") & F.from_user.is_premium)\n",[113,1297,1298],{"class":115,"line":308},[113,1299,180],{"emptyLinePlaceholder":179},[113,1301,1302],{"class":115,"line":313},[113,1303,1304],{},"# Отрицание\n",[113,1306,1307],{"class":115,"line":319},[113,1308,1309],{},"@router.message(~F.text.in_({\"стоп\", \"отмена\"}))\n",[11,1311,1312,1313,1316],{},"Больше никаких ",[66,1314,1315],{},"if message.text == \"Купить\":"," внутри хендлеров. Фильтры объявлены декларативно — их легко читать и комбинировать.",[41,1318,1320],{"id":1319},"инлайн-клавиатуры-и-колбэки","Инлайн-клавиатуры и колбэки",[104,1322,1324],{"className":158,"code":1323,"language":160,"meta":109,"style":109},"# 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=\"📦 Товары\", callback_data=\"products\")\n    b.button(text=\"📋 Мои заказы\", callback_data=\"my_orders\")\n    b.button(text=\"💬 Контакт\", url=\"https:\u002F\u002Ft.me\u002Fyourusername\")\n    b.adjust(2, 1)\n    return b.as_markup()\n",[66,1325,1326,1331,1336,1341,1345,1350,1355,1360,1365,1370,1375],{"__ignoreMap":109},[113,1327,1328],{"class":115,"line":116},[113,1329,1330],{},"# keyboards\u002Fkb.py\n",[113,1332,1333],{"class":115,"line":170},[113,1334,1335],{},"from aiogram.utils.keyboard import InlineKeyboardBuilder\n",[113,1337,1338],{"class":115,"line":176},[113,1339,1340],{},"from aiogram.types import InlineKeyboardMarkup\n",[113,1342,1343],{"class":115,"line":183},[113,1344,180],{"emptyLinePlaceholder":179},[113,1346,1347],{"class":115,"line":189},[113,1348,1349],{},"def main_kb() -> InlineKeyboardMarkup:\n",[113,1351,1352],{"class":115,"line":195},[113,1353,1354],{},"    b = InlineKeyboardBuilder()\n",[113,1356,1357],{"class":115,"line":201},[113,1358,1359],{},"    b.button(text=\"📦 Товары\", callback_data=\"products\")\n",[113,1361,1362],{"class":115,"line":207},[113,1363,1364],{},"    b.button(text=\"📋 Мои заказы\", callback_data=\"my_orders\")\n",[113,1366,1367],{"class":115,"line":212},[113,1368,1369],{},"    b.button(text=\"💬 Контакт\", url=\"https:\u002F\u002Ft.me\u002Fyourusername\")\n",[113,1371,1372],{"class":115,"line":218},[113,1373,1374],{},"    b.adjust(2, 1)\n",[113,1376,1377],{"class":115,"line":224},[113,1378,1379],{},"    return b.as_markup()\n",[104,1381,1383],{"className":158,"code":1382,"language":160,"meta":109,"style":109},"# handler\nfrom aiogram.types import CallbackQuery\n\n@router.message(CommandStart())\nasync def cmd_start(message: Message):\n    await message.answer(\"Выберите:\", reply_markup=main_kb())\n\n@router.callback_query(F.data == \"products\")\nasync def show_products(callback: CallbackQuery):\n    await callback.answer()\n    await callback.message.edit_text(\"Вот наши товары...\", reply_markup=products_kb())\n",[66,1384,1385,1390,1395,1399,1403,1407,1412,1416,1421,1426,1431],{"__ignoreMap":109},[113,1386,1387],{"class":115,"line":116},[113,1388,1389],{},"# handler\n",[113,1391,1392],{"class":115,"line":170},[113,1393,1394],{},"from aiogram.types import CallbackQuery\n",[113,1396,1397],{"class":115,"line":176},[113,1398,180],{"emptyLinePlaceholder":179},[113,1400,1401],{"class":115,"line":183},[113,1402,380],{},[113,1404,1405],{"class":115,"line":189},[113,1406,385],{},[113,1408,1409],{"class":115,"line":195},[113,1410,1411],{},"    await message.answer(\"Выберите:\", reply_markup=main_kb())\n",[113,1413,1414],{"class":115,"line":201},[113,1415,180],{"emptyLinePlaceholder":179},[113,1417,1418],{"class":115,"line":207},[113,1419,1420],{},"@router.callback_query(F.data == \"products\")\n",[113,1422,1423],{"class":115,"line":212},[113,1424,1425],{},"async def show_products(callback: CallbackQuery):\n",[113,1427,1428],{"class":115,"line":218},[113,1429,1430],{},"    await callback.answer()\n",[113,1432,1433],{"class":115,"line":224},[113,1434,1435],{},"    await callback.message.edit_text(\"Вот наши товары...\", reply_markup=products_kb())\n",[11,1437,1438,1439,1442],{},"Всегда вызывайте ",[66,1440,1441],{},"callback.answer()"," первой — без неё кнопка остаётся в состоянии загрузки 30 секунд.",[41,1444,1446],{"id":1445},"что-дальше","Что дальше",[11,1448,1449,1450,32],{},"Когда первый бот уже работает, следующий шаг — база данных, платежи, Mini App и продакшен-деплой. Это разобрано в ",[23,1451,1452],{"href":30},"полном курсе по Telegram-ботам на Python",[41,1454,1455],{"id":408},"FSM: многошаговые диалоги",[11,1457,1458],{},"FSM (конечный автомат) — это то, как обрабатывать сценарии, которые длятся несколько сообщений: формы заказа, регистрация, опросы.",[104,1460,1462],{"className":158,"code":1461,"language":160,"meta":109,"style":109},"# 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",[66,1463,1464,1469,1473,1477,1481,1486,1491],{"__ignoreMap":109},[113,1465,1466],{"class":115,"line":116},[113,1467,1468],{},"# states\u002Fstates.py\n",[113,1470,1471],{"class":115,"line":170},[113,1472,424],{},[113,1474,1475],{"class":115,"line":176},[113,1476,180],{"emptyLinePlaceholder":179},[113,1478,1479],{"class":115,"line":183},[113,1480,433],{},[113,1482,1483],{"class":115,"line":189},[113,1484,1485],{},"    waiting_name = State()\n",[113,1487,1488],{"class":115,"line":195},[113,1489,1490],{},"    waiting_phone = State()\n",[113,1492,1493],{"class":115,"line":201},[113,1494,1495],{},"    waiting_description = State()\n",[104,1497,1499],{"className":158,"code":1498,"language":160,"meta":109,"style":109},"# 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(\"Как вас зовут?\")\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(\"Ваш номер телефона:\")\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(\"Опишите задачу:\")\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\"✅ Заказ принят!\\n\"\n        f\"Имя: {data['name']}\\n\"\n        f\"Телефон: {data['phone']}\\n\"\n        f\"Задача: {message.text}\"\n    )\n",[66,1500,1501,1506,1510,1515,1519,1524,1529,1533,1537,1542,1546,1550,1554,1558,1562,1567,1571,1576,1581,1587,1593,1599,1604,1610,1616,1622,1628,1633,1639,1645,1651,1657],{"__ignoreMap":109},[113,1502,1503],{"class":115,"line":116},[113,1504,1505],{},"# handlers\u002Forders.py\n",[113,1507,1508],{"class":115,"line":170},[113,1509,419],{},[113,1511,1512],{"class":115,"line":176},[113,1513,1514],{},"from states.states import OrderForm\n",[113,1516,1517],{"class":115,"line":183},[113,1518,180],{"emptyLinePlaceholder":179},[113,1520,1521],{"class":115,"line":189},[113,1522,1523],{},"@router.callback_query(F.data == \"new_order\")\n",[113,1525,1526],{"class":115,"line":195},[113,1527,1528],{},"async def start_order(callback: CallbackQuery, state: FSMContext):\n",[113,1530,1531],{"class":115,"line":201},[113,1532,467],{},[113,1534,1535],{"class":115,"line":207},[113,1536,1430],{},[113,1538,1539],{"class":115,"line":212},[113,1540,1541],{},"    await callback.message.answer(\"Как вас зовут?\")\n",[113,1543,1544],{"class":115,"line":218},[113,1545,180],{"emptyLinePlaceholder":179},[113,1547,1548],{"class":115,"line":224},[113,1549,481],{},[113,1551,1552],{"class":115,"line":229},[113,1553,486],{},[113,1555,1556],{"class":115,"line":302},[113,1557,491],{},[113,1559,1560],{"class":115,"line":308},[113,1561,496],{},[113,1563,1564],{"class":115,"line":313},[113,1565,1566],{},"    await message.answer(\"Ваш номер телефона:\")\n",[113,1568,1569],{"class":115,"line":319},[113,1570,180],{"emptyLinePlaceholder":179},[113,1572,1573],{"class":115,"line":324},[113,1574,1575],{},"@router.message(OrderForm.waiting_phone)\n",[113,1577,1578],{"class":115,"line":330},[113,1579,1580],{},"async def got_phone(message: Message, state: FSMContext):\n",[113,1582,1584],{"class":115,"line":1583},19,[113,1585,1586],{},"    await state.update_data(phone=message.text)\n",[113,1588,1590],{"class":115,"line":1589},20,[113,1591,1592],{},"    await state.set_state(OrderForm.waiting_description)\n",[113,1594,1596],{"class":115,"line":1595},21,[113,1597,1598],{},"    await message.answer(\"Опишите задачу:\")\n",[113,1600,1602],{"class":115,"line":1601},22,[113,1603,180],{"emptyLinePlaceholder":179},[113,1605,1607],{"class":115,"line":1606},23,[113,1608,1609],{},"@router.message(OrderForm.waiting_description)\n",[113,1611,1613],{"class":115,"line":1612},24,[113,1614,1615],{},"async def got_description(message: Message, state: FSMContext):\n",[113,1617,1619],{"class":115,"line":1618},25,[113,1620,1621],{},"    data = await state.get_data()\n",[113,1623,1625],{"class":115,"line":1624},26,[113,1626,1627],{},"    await state.clear()\n",[113,1629,1631],{"class":115,"line":1630},27,[113,1632,390],{},[113,1634,1636],{"class":115,"line":1635},28,[113,1637,1638],{},"        f\"✅ Заказ принят!\\n\"\n",[113,1640,1642],{"class":115,"line":1641},29,[113,1643,1644],{},"        f\"Имя: {data['name']}\\n\"\n",[113,1646,1648],{"class":115,"line":1647},30,[113,1649,1650],{},"        f\"Телефон: {data['phone']}\\n\"\n",[113,1652,1654],{"class":115,"line":1653},31,[113,1655,1656],{},"        f\"Задача: {message.text}\"\n",[113,1658,1660],{"class":115,"line":1659},32,[113,1661,405],{},[11,1663,1664,1667,1668,1671,1672,32],{},[66,1665,1666],{},"FSMContext"," автоматически внедряется Aiogram. Состояние хранится между сообщениями. По умолчанию используется ",[66,1669,1670],{},"MemoryStorage"," (теряется при перезапуске) — для продакшна переключайтесь на ",[66,1673,1674],{},"RedisStorage",[41,1676,1678],{"id":1677},"middleware-код-на-каждое-обновление","Middleware: код на каждое обновление",[104,1680,1682],{"className":158,"code":1681,"language":160,"meta":109,"style":109},"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\"Обновление от пользователя {event.from_user.id}\")\n        return await handler(event, data)\n\ndp.update.middleware(LoggingMiddleware())\n",[66,1683,1684,1689,1694,1698,1703,1708,1713,1718,1722],{"__ignoreMap":109},[113,1685,1686],{"class":115,"line":116},[113,1687,1688],{},"from aiogram import BaseMiddleware\n",[113,1690,1691],{"class":115,"line":170},[113,1692,1693],{},"from aiogram.types import TelegramObject\n",[113,1695,1696],{"class":115,"line":176},[113,1697,180],{"emptyLinePlaceholder":179},[113,1699,1700],{"class":115,"line":183},[113,1701,1702],{},"class LoggingMiddleware(BaseMiddleware):\n",[113,1704,1705],{"class":115,"line":189},[113,1706,1707],{},"    async def __call__(self, handler, event: TelegramObject, data: dict):\n",[113,1709,1710],{"class":115,"line":195},[113,1711,1712],{},"        print(f\"Обновление от пользователя {event.from_user.id}\")\n",[113,1714,1715],{"class":115,"line":201},[113,1716,1717],{},"        return await handler(event, data)\n",[113,1719,1720],{"class":115,"line":207},[113,1721,180],{"emptyLinePlaceholder":179},[113,1723,1724],{"class":115,"line":212},[113,1725,1726],{},"dp.update.middleware(LoggingMiddleware())\n",[11,1728,1729],{},"Я использую middleware чтобы автоматом сохранять пользователей в базу при первом контакте — это выполняется до любого хендлера, никогда не забудешь добавить.",[41,1731,1733],{"id":1732},"деплой-systemd-на-vps","Деплой: systemd на VPS",[11,1735,1736],{},"Простейшая продакшн-установка: Ubuntu VPS, systemd сервис. Docker не нужен для простых ботов.",[104,1738,1742],{"className":1739,"code":1740,"language":1741,"meta":109,"style":109},"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",[66,1743,1744,1749,1754,1759,1764,1768,1773,1778,1783,1788,1793,1798,1803,1807,1812],{"__ignoreMap":109},[113,1745,1746],{"class":115,"line":116},[113,1747,1748],{},"# \u002Fetc\u002Fsystemd\u002Fsystem\u002Fmybot.service\n",[113,1750,1751],{"class":115,"line":170},[113,1752,1753],{},"[Unit]\n",[113,1755,1756],{"class":115,"line":176},[113,1757,1758],{},"Description=My Telegram Bot\n",[113,1760,1761],{"class":115,"line":183},[113,1762,1763],{},"After=network.target\n",[113,1765,1766],{"class":115,"line":189},[113,1767,180],{"emptyLinePlaceholder":179},[113,1769,1770],{"class":115,"line":195},[113,1771,1772],{},"[Service]\n",[113,1774,1775],{"class":115,"line":201},[113,1776,1777],{},"User=ubuntu\n",[113,1779,1780],{"class":115,"line":207},[113,1781,1782],{},"WorkingDirectory=\u002Fhome\u002Fubuntu\u002Fmybot\n",[113,1784,1785],{"class":115,"line":212},[113,1786,1787],{},"Environment=\"BOT_TOKEN=your_token\"\n",[113,1789,1790],{"class":115,"line":218},[113,1791,1792],{},"ExecStart=\u002Fhome\u002Fubuntu\u002Fmybot\u002F.venv\u002Fbin\u002Fpython bot.py\n",[113,1794,1795],{"class":115,"line":224},[113,1796,1797],{},"Restart=always\n",[113,1799,1800],{"class":115,"line":229},[113,1801,1802],{},"RestartSec=5\n",[113,1804,1805],{"class":115,"line":302},[113,1806,180],{"emptyLinePlaceholder":179},[113,1808,1809],{"class":115,"line":308},[113,1810,1811],{},"[Install]\n",[113,1813,1814],{"class":115,"line":313},[113,1815,1816],{},"WantedBy=multi-user.target\n",[104,1818,1820],{"className":106,"code":1819,"language":108,"meta":109,"style":109},"sudo systemctl enable mybot\nsudo systemctl start mybot\nsudo systemctl status mybot\njournalctl -u mybot -f\n",[66,1821,1822,1836,1847,1858],{"__ignoreMap":109},[113,1823,1824,1827,1830,1833],{"class":115,"line":116},[113,1825,1826],{"class":119},"sudo",[113,1828,1829],{"class":123}," systemctl",[113,1831,1832],{"class":123}," enable",[113,1834,1835],{"class":123}," mybot\n",[113,1837,1838,1840,1842,1845],{"class":115,"line":170},[113,1839,1826],{"class":119},[113,1841,1829],{"class":123},[113,1843,1844],{"class":123}," start",[113,1846,1835],{"class":123},[113,1848,1849,1851,1853,1856],{"class":115,"line":176},[113,1850,1826],{"class":119},[113,1852,1829],{"class":123},[113,1854,1855],{"class":123}," status",[113,1857,1835],{"class":123},[113,1859,1860,1863,1866,1869],{"class":115,"line":183},[113,1861,1862],{"class":119},"journalctl",[113,1864,1865],{"class":130}," -u",[113,1867,1868],{"class":123}," mybot",[113,1870,1871],{"class":130}," -f\n",[11,1873,1874],{},"Бот теперь переживает перезагрузки VPS и автоматически перезапускается после падений.",[41,1876,1878],{"id":1877},"что-изучать-дальше","Что изучать дальше",[11,1880,1881],{},"Когда освоите базу:",[46,1883,1884,1890,1904,1910,1916],{},[49,1885,1886,1889],{},[15,1887,1888],{},"PostgreSQL + asyncpg"," — большинству ботов рано или поздно нужна база данных",[49,1891,1892,920,1895,924,1898,924,1901],{},[15,1893,1894],{},"Telegram Stars платежи",[66,1896,1897],{},"send_invoice()",[66,1899,1900],{},"pre_checkout_query",[66,1902,1903],{},"successful_payment",[49,1905,1906,1909],{},[15,1907,1908],{},"Webhooks"," — быстрее чем polling для высоконагруженных ботов, нужен HTTPS домен",[49,1911,1912,1915],{},[15,1913,1914],{},"Telegram Mini App"," — полноценный веб-интерфейс внутри Telegram",[49,1917,1918,1921],{},[15,1919,1920],{},"Система рассылок"," — отправка сообщений всем пользователям без превышения лимитов",[11,1923,1924,1925,1930,1931,1936],{},"Документация на ",[23,1926,1929],{"href":1927,"rel":1928},"https:\u002F\u002Fdocs.aiogram.dev",[976],"docs.aiogram.dev"," отличная. Telegram-чат Aiogram (",[23,1932,1935],{"href":1933,"rel":1934},"https:\u002F\u002Ft.me\u002Faiogram",[976],"@aiogram",") активный и полезный.",[11,1938,1939],{},"Сделайте реальный проект, запустите его, смотрите как работает. Только так реально учишься.",[839,1941,1942],{},"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 .sj4cs, html code.shiki .sj4cs{--shiki-default:#005CC5;--shiki-dark:#79B8FF}",{"title":109,"searchDepth":170,"depth":170,"links":1944},[1945,1946,1947,1948,1949,1950,1951,1952,1953,1954,1955],{"id":896,"depth":170,"text":897},{"id":953,"depth":170,"text":954},{"id":1015,"depth":170,"text":1016},{"id":1108,"depth":170,"text":1109},{"id":1221,"depth":170,"text":1222},{"id":1319,"depth":170,"text":1320},{"id":1445,"depth":170,"text":1446},{"id":408,"depth":170,"text":1455},{"id":1677,"depth":170,"text":1678},{"id":1732,"depth":170,"text":1733},{"id":1877,"depth":170,"text":1878},"2025-04-10","Пошаговое руководство по созданию Telegram-бота на Aiogram 3 — хендлеры, FSM, инлайн-клавиатуры, деплой. Без воды.",{},"\u002Fru\u002Fblog\u002Faiogram-3-tutorial-2025","Aiogram 3 туториал",[867],{"title":881,"description":1957},"aiogram-3-tutorial-2025","ru\u002Fblog\u002Faiogram-3-tutorial-2025",[876,874,1966,875,1967],"Туториал","Боты","uPtvnu-dtXvbzVODoGVmSqH6gWV5tYc--3BYTZMnShg",{"id":1970,"title":1971,"body":1972,"cluster":855,"ctaType":856,"date":877,"description":2129,"extension":859,"intent":860,"meta":2130,"navigation":179,"ogImage":862,"path":2131,"primaryKeyword":2132,"readTime":2133,"relatedCases":2134,"relatedCourse":2135,"relatedService":869,"seo":2136,"slug":2137,"stem":2138,"tags":2139,"updated":2141,"__hash__":2142},"blogRu\u002Fru\u002Fblog\u002Fproduction-struktura-aiogram.md","Production-структура проекта на Aiogram 3: handlers, services и конфигурация",{"type":8,"value":1973,"toc":2122},[1974,1977,1981,1987,1993,1997,2000,2033,2043,2047,2050,2054,2065,2068,2072,2101,2111,2119],[11,1975,1976],{},"Надёжная структура Aiogram 3 отделяет Telegram-слой от бизнес-логики: handlers принимают обновление, services выполняют действие, repositories работают с данными, а config отвечает за окружение. Это упрощает тестирование и не даёт платежам, FSM и уведомлениям переплестись в одном файле.",[41,1978,1980],{"id":1979},"минимальная-структура","Минимальная структура",[104,1982,1985],{"className":1983,"code":1984,"language":149,"meta":109},[147],"app\u002F\n  bot.py\n  config.py\n  handlers\u002F\n    start.py\n    orders.py\n  services\u002F\n    orders.py\n  repositories\u002F\n    orders.py\n  keyboards\u002F\n  middlewares\u002F\ntests\u002F\n",[66,1986,1984],{"__ignoreMap":109},[11,1988,1989,1992],{},[66,1990,1991],{},"bot.py"," собирает приложение и подключает роутеры. В handler не нужно помещать SQL-запросы и сложные расчёты. Он должен достать входные данные, вызвать сервис и выбрать ответ пользователю.",[41,1994,1996],{"id":1995},"конфигурация-через-окружение","Конфигурация через окружение",[11,1998,1999],{},"Токен и ключи интеграций не должны попадать в код или репозиторий. Минимальная конфигурация проверяет обязательные значения на старте:",[104,2001,2003],{"className":158,"code":2002,"language":160,"meta":109,"style":109},"from pydantic_settings import BaseSettings, SettingsConfigDict\n\nclass Settings(BaseSettings):\n    model_config = SettingsConfigDict(env_file=\".env\", extra=\"ignore\")\n    bot_token: str\n    database_url: str = \"sqlite+aiosqlite:\u002F\u002F\u002F.\u002Fbot.db\"\n",[66,2004,2005,2010,2014,2018,2023,2028],{"__ignoreMap":109},[113,2006,2007],{"class":115,"line":116},[113,2008,2009],{},"from pydantic_settings import BaseSettings, SettingsConfigDict\n",[113,2011,2012],{"class":115,"line":170},[113,2013,180],{"emptyLinePlaceholder":179},[113,2015,2016],{"class":115,"line":176},[113,2017,186],{},[113,2019,2020],{"class":115,"line":183},[113,2021,2022],{},"    model_config = SettingsConfigDict(env_file=\".env\", extra=\"ignore\")\n",[113,2024,2025],{"class":115,"line":189},[113,2026,2027],{},"    bot_token: str\n",[113,2029,2030],{"class":115,"line":195},[113,2031,2032],{},"    database_url: str = \"sqlite+aiosqlite:\u002F\u002F\u002F.\u002Fbot.db\"\n",[11,2034,2035,2036,2039,2040,2042],{},"В репозитории хранится только ",[66,2037,2038],{},".env.example",". Реальный ",[66,2041,68],{}," добавляется на сервере или в секреты CI.",[41,2044,2046],{"id":2045},"роутеры-вместо-большого-handler-файла","Роутеры вместо большого handler-файла",[11,2048,2049],{},"Группируй обработчики по предметной области: заявки, каталог, профиль, платежи. Роутер не должен знать, где физически хранится заявка. Это позволяет заменить SQLite на PostgreSQL без переписывания Telegram-слоя.",[41,2051,2053],{"id":2052},"services-и-repositories","Services и repositories",[11,2055,2056,2057,2060,2061,2064],{},"Сервис отвечает на вопрос «что сделать», repository — «как прочитать или сохранить данные». Например, ",[66,2058,2059],{},"orders.create()"," проверяет бизнес-правила, а ",[66,2062,2063],{},"orders_repository.insert()"," выполняет запрос. Внешние API вызываются из отдельного gateway с timeout, повторными попытками и логированием request id.",[11,2066,2067],{},"Такой разнос особенно важен для оплаты. Webhook может прийти повторно, поэтому обработчик сначала проверяет idempotency key, затем обновляет заказ и только после этого выдаёт доступ.",[41,2069,2071],{"id":2070},"что-проверить-перед-деплоем","Что проверить перед деплоем",[46,2073,2074,2077,2082,2085,2092,2095,2098],{},[49,2075,2076],{},"запуск проходит без ручного импорта из рабочей директории;",[49,2078,2079,2081],{},[66,2080,2038],{}," описывает все переменные;",[49,2083,2084],{},"polling и webhook не включены одновременно;",[49,2086,2087,2088,2091],{},"ошибки внешних API не теряются в пустом ",[66,2089,2090],{},"except",";",[49,2093,2094],{},"повторный update не создаёт дубль заявки;",[49,2096,2097],{},"есть health endpoint и понятный журнал событий;",[49,2099,2100],{},"критические handlers покрыты тестами.",[11,2102,2103,2104,2106,2107,2110],{},"Для первого проекта можно начать с ",[23,2105,892],{"href":25},", а затем перейти к ",[23,2108,2109],{"href":30},"полному курсу по Telegram-ботам",", где есть платежи, Mini App, webhook, Docker и production-проверки.",[11,2112,2113,2114,2118],{},"Если нужна такая архитектура для реального продукта, ",[23,2115,2117],{"href":2116},"\u002Fru\u002F#contact","опиши задачу в заявке",": достаточно указать основной сценарий, интеграции и предполагаемую нагрузку.",[839,2120,2121],{},"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);}",{"title":109,"searchDepth":170,"depth":170,"links":2123},[2124,2125,2126,2127,2128],{"id":1979,"depth":170,"text":1980},{"id":1995,"depth":170,"text":1996},{"id":2045,"depth":170,"text":2046},{"id":2052,"depth":170,"text":2053},{"id":2070,"depth":170,"text":2071},"Как разложить Telegram-бота на Aiogram 3 по слоям, чтобы добавлять FSM, платежи и интеграции без единого огромного файла.",{},"\u002Fru\u002Fblog\u002Fproduction-struktura-aiogram","структура проекта Aiogram 3","10 мин",[867],"telegram-bots-python",{"title":1971,"description":2129},"production-struktura-aiogram","ru\u002Fblog\u002Fproduction-struktura-aiogram",[876,874,2140,875],"Архитектура",null,"wLBL2iyeP47FfRO8Uk8ZGBpG1A8n0AXd3TaDYKFwuv4",1787941381677]