惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

P
Proofpoint News Feed
罗磊的独立博客
Martin Fowler
Martin Fowler
B
Blog RSS Feed
U
Unit 42
V
V2EX
H
Help Net Security
V
Vulnerabilities – Threatpost
The Register - Security
The Register - Security
IT之家
IT之家
Cloudbric
Cloudbric
K
Kaspersky official blog
C
CXSECURITY Database RSS Feed - CXSecurity.com
Jina AI
Jina AI
Microsoft Azure Blog
Microsoft Azure Blog
大猫的无限游戏
大猫的无限游戏
P
Palo Alto Networks Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
S
SegmentFault 最新的问题
Cisco Talos Blog
Cisco Talos Blog
博客园 - 司徒正美
Cyberwarzone
Cyberwarzone
Spread Privacy
Spread Privacy
P
Proofpoint News Feed
Apple Machine Learning Research
Apple Machine Learning Research
T
Tenable Blog
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
I
Intezer
G
GRAHAM CLULEY
T
Threatpost
C
Check Point Blog
C
Cyber Attacks, Cyber Crime and Cyber Security
H
Hackread – Cybersecurity News, Data Breaches, AI and More
D
Darknet – Hacking Tools, Hacker News & Cyber Security
T
Tor Project blog
L
Lohrmann on Cybersecurity
WordPress大学
WordPress大学
N
Netflix TechBlog - Medium
The GitHub Blog
The GitHub Blog
C
CERT Recently Published Vulnerability Notes
Microsoft Security Blog
Microsoft Security Blog
Hugging Face - Blog
Hugging Face - Blog
The Hacker News
The Hacker News
Schneier on Security
Schneier on Security
爱范儿
爱范儿
云风的 BLOG
云风的 BLOG
Security Archives - TechRepublic
Security Archives - TechRepublic
T
The Blog of Author Tim Ferriss
K
KPMG report finds enterprise disconnect between AI and its ROI | CIO
P
Privacy & Cybersecurity Law Blog

Все публикации подряд на Хабре

Ловим музу за клавиатуру: как айтишнику стать автором Что умеет Midjourney в 2026? Мой немного грустный разбор этого шикарного инструмента Никто не любит писать тесты, но ИИ может исправить это IPv8 выглядит как мечта. Поэтому почти наверняка не взлетит Производители вернули в продажу материнки с DDR3. Что происходит? Управление агентом с телефона через Telegram теперь в KodaCode От координации к лидерству: как меняется роль руководителя разработки Я сделала родителям бизнес вместо пенсии: зарабатываем 70 тысяч, мама не даёт продать В три раза быстрее приемка товара и оптимизация трудозатрат на 73%: как «РСТ-Инвент» помог Gulliver Group ИИ-шечный мир победил? О влиянии искусственного интеллекта на игропром Кремль снижает давление на Телеграмм пока Европа строит интернет по паспорту Как CEO, CTO и CIO за 8 часов собрали ИИ-директора, который умеет держать позицию под давлением Как (не) потерять домен за выходные Вместо 8 разных VPS: как я организовал практику студентам на одном сервере Почему твой Open Source проект не замечают? R&D: искусство управления неопределенностью в разработке AI-дефляция: вакансий для разработчиков больше, а рост зарплат — худший за 15 лет Мы отдали управление роботами OpenClaw. Что из этого вышло Галактический ID: система идентификации для всех форм разумной жизни Шесть основ бизнес-анализа: начинаем с вопроса «Кто в игре?» Код-ревью, в котором дело не в коде Данные переехали. Команда — нет Системной подход к сдаче OSWE в 2025 Почему комната управления реактором покрашена в цвет морской пены 4 YAML-файла вместо PySpark: как аналитикам строить пайплайны без разработчиков LLM-агент для поиска свободных доменов: автоматизируем подбор Когда, зачем и как правильно начинать новую сессию в Claude Code? Как я заставил нейросеть писать макросы для FreeCAD Анатомия ИИ‑агента для подбора персонала. От тысячи резюме к топ‑10 за минуты Опыт разработчика как экономика внимания Автономность как точка невозврата: кто будет субъектом в цифровом будущем Обучение ИИ в «диких» условиях: как рутинные действия превращаются в датасеты Как измерить LLM для задач кибербеза: обзор открытых бенчмарков Где хранить код? Сравнение GitHub, GitLab и Bitbucket Математика объясняет, почему нормальное распределение встречается повсюду Почему ваш FinOps не работает: 12 тезисов от практиков Как подписать проектную документацию УКЭП с использованием бесплатных лицензий Pilot Адаптивное администрирование Sigla Vision Я грузил уран в бочки, а потом 20 лет строил ИТ в атомной отрасли Чем позвонить с Эвереста? История и обзор спутниковой связи. Часть 2 Как языковая модель помогает контролировать качество инструктажей по охране труда в металлургии Как не передать на desktop свой IP в РКН Анатомия SAP Privileges: как устроено управление правами в macOS MoneyDev: Сказка про три главных слова Обновлённый токенизатор видео K-VAE 2.0 от Сбера Как сделать диспетчеризацию дома на 1284 квартиры почти бесплатно Как мы разогнали железную дорогу Мы дали агентам рутину. Теперь надо решить — что делать с освободившимся временем Токсичный контент, промпт-хакинг и защита ИИ — всё о Guardrails для LLM Умный город начинается с точного взгляда: как «Фалькон Тех» меняет пространство к лучшему Навайбкодил приложение для анализа графов Почему Дюну так интересно читать? Упрощаем работу с рутиной или как стать Гендальфом Белым Деконструкция Go: CPU, RAM и что там происходит. Go Assembler база. Часть 1.1 Какие профессии исчезнут из-за ИИ, а какие появятся? И что с этим делать Как мы построили IT-отдел, где хочется расти: архитектурные встречи, прозрачные метрики и книжные подарки Rufler: Делаем из Claude Code автономный рой через один YAML-конфиг Sing-box и белый список приложений Как построить надёжный обмен сообщениями в микросервисах: лучшие практики для enterprise OpenAI строит MLM-пирамиду, а McKinsey и Accenture помогают ей в этом Дом, который не построил Фишер (Часть 2) «Сверхзвуковой математик» против «Вдумчивого логиста»: битва алгоритмов 3D-упаковки Мультимодальные модели – грубый и дорогой инструмент Разговоры ничего не стоят. Код тоже Проверки физических лиц: с кого начнет ФНС Топ-10 бесплатных нейросетей для создания видео в 2026 году Первые слои кода: как наши решения сегодня определяют архитектуру ИИ на десятилетия Разработка нового статического анализатора: PVS-Studio JavaScript Поиск уязвимостей ПО: базовый минимум или роскошный максимум Почему оценка персонала не работает как инструмент управления Как мы разработали ИИ-ассистента и сократили рутину продуктовой команды на 50% Как я ушел из найма, нажарил косточек и продал на маркетплейсах на 168 млн в год Когда 1С:ERP уже внедрена, а нормального производственного плана всё ещё нет Как я сделал Claude мультимодальным, подключив к нему Qwen Omni Как приглашение на вакансию мечты превращается в атаку Infrastructure as Code: философия и лучшие практики IaC Тестируем Yandex Code Assistant на задаче, в которой нужно хранить секреты nxs-universal-chart v3.0: новое поколение универсального Helm-чарта Callback Injection: Техника, которая отправила Microsoft Defender в глухой нокаут «Все идеи на стол»: митап как способ вывести проект из тупика Сегодня я узнал нечто новое о GPU благодаря багу в своей игре Как заставить LLM ̶ ̶г̶а̶л̶л̶ю̶ ̶ эволюционировать Карта событий как фундамент аналитики: практический кейс для E-commerce Что выбрать для AI: x86, ARM или RISC-V? Дайджест железа за март Роль соматических мутаций в развитии аутоиммунных заболеваний: путь к избирательной терапии Mythos от Anthropic — тревожный сигнал для всех, а не только для банков Guardrails для LLM на Java: как приручить промпт‑инъекции и токсичные ответы Green-VLA: как мы собрали VLA-модель для реального антропоморфного робота и не потеряли обобщение Финансовая гонка вооружений: почему умные люди добровольно в ней участвуют Эра ИИ-агентов наступила: выбираем лучшего цифрового сотрудника # Практический опыт внедрения WinCC Redundancy на производственном предприятии Сделал MVP за 3 дня, а потом неделю прикручивал оплату. Оно того стоило? Физика против Маска: почему Starship V3 может оказаться ещё одной катастрофой Нефть Венесуэлы: крупнейшие запасы в мире, но не крупнейшая нефтяная держава JPA 4. Переосмысление Hibernate Почему зеркальная фотокамера Nikon D5 десятилетней давности идеально подошла для миссии «Артемида-2» Проект «Уровень-Спутник» или как мы сделали платформу для гидрологов «Замедлиться, чтобы ускориться»: почему ИИ повышает цену ошибок в требованиях и архитектуре Как с нуля поднять трафик IT-компании на 1657% при бюджете 55 тыс. и выжить Pixel-perfect Downsampling — идеальная отрисовка 50 миллионов точек без потерь
Я заколебался искать запятую в коде бота — и написал библиотеку, чтобы диалоги жили в YAML
Иван Моргун · 2026-05-15 · via Все публикации подряд на Хабре

Средний

12 мин

9.1K

Дисклеймер: статья будет про Telegram-бота на Python и aiogram. Если ты пишешь ботов на чём-то ещё — листай, тут тебя расстроят. Если не пишешь вообще — оставайся, иногда полезно посмотреть, как другие страдают.

Привет. Я три года пилю Telegram-бот для одного B2C-продукта по подписке. Бот, как полагается всякому продуктовому боту, оброс примерно 800 строками диалогов: онбординги, оплаты, инструкции на iOS/Android/винде, тех-поддержка, рассылки, ошибки, благодарности, апселл премиума, реферальная программа, A/B-тесты копирайтинга, и три варианта одной и той же кнопки «Подключиться», потому что маркетолог каждый квартал хочет «ну вот тут чуть-чуть подвинуть».

И всё это, как у всех нормальных людей, лежало в Python-коде.

Не «в коде» в смысле «у нас красивые тексты в одном файле texts.py». А в коде в смысле — в хендлерах, в утилитах, в сервисах, кусочно, копипастно, с f-строками, с условиями if user.is_premium:, с тремя разными способами собрать одну и ту же inline-клавиатуру, и с пятым по счёту «ой, не забудьте parse_mode='HTML', иначе у нас бот выплёвывает голый <b> пользователю».

И вот в один прекрасный пятничный вечер маркетолог пишет:

«Слушай, в тексте после оплаты можно убрать лишнюю запятую? Ну там «Поздравляем, ваша подписка активна» — две запятые, не очень.»

А я такой:

  • 🔎 grep’аю по проекту «Поздравляем»

  • нахожу семь мест: в payment_handler.py, в success_paywall.py, в notifications/payment_success.py, в админ-команде «выдать промо-доступ», в утилите рассылки, в тестовом сценарии, и ещё в одном месте, где это вообще «Поздравлеям» — кто-то когда-то опечатался, и эта ветка просто никогда не тригерилась

  • начинаю править

  • в одном месте текст склеен через + из трёх кусков, потому что там вставляется дата окончания подписки

  • в другом месте текст шаблонизирован через .format(), но по-другому

  • в третьем месте текст лежит в YAML-конфиге (ха-ха, единственный приличный человек в команде когда-то так и сделал)

Я потратил двадцать минут на одну запятую.

И тут меня переклинило.

«Нужно что-то менять»

Ну правда: я инженер, я могу 10 раз в день деплоить, я умею в CI/CD, я знаю про feature flags. Но текст в боте — это, оказывается, нифига не «деплоить десять раз в день». Это «помолись, чтобы ты нашёл правильное место, не зацепил ничего лишнего, и чтобы прод не лёг, потому что ты случайно вместо <b> написал <bb>».

Самое смешное, что боль не моя личная. Я начал общаться с другими ребятами, кто пилит ботов:

  • У одних тексты в Python, копипаст клавиатур на 12 файлов, любое изменение брендинга — это PR на 200 строк диффа.

  • У других всё в i18n-файлах (messages.ru.json), но клавиатуры всё равно собираются в Python — и теперь у тебя «текст в одном месте, кнопки в другом», и менять их синхронно — отдельный квест.

  • У третьих — святой Грааль — реализован свой DSL поверх Python, тексты живут в dataclass’ах с методом to_markup(), всё круто, но порог входа для нового разраба — три дня.

  • А у четвёртых вообще «зачем нам это, у нас и так норм».

И тут я подумал: окей, а как бы я в идеале хотел?

Мне нужно:

  1. Один файл, где живут тексты + кнопки + картинки для каждой реплики бота. Не два. Не семь. Один.

  2. Чтобы маркетолог мог в этот файл залезть, поправить запятую, и не упасть в кому от вида f-строк.

  3. Чтобы можно было переиспользовать куски: ссылка на поддержку — это одно место, а не пятнадцать.

  4. Чтобы можно было сказать «вот эта кнопка показывается только премиум-юзерам», но в одном файле, без if-ов в трёх хендлерах.

  5. Чтобы поддерживались все методы Telegram — не только send_message, а и фотки, и видео, и опросы, и геолокации.

  6. Чтобы оно умело не положить бота при рассылках — Telegram ставит лимит 30 msg/s на бота, и при превышении огребаешь 429 Too Many Requests.

И вот тут я понял: я хочу YAML. Прямо как Kubernetes, Docker Compose, GitHub Actions — все же научились декларативно описывать систему, почему бы и боту — нельзя?

Знакомьтесь, aiogram-dialog-yaml

Это библиотека. Питоновская. Лежит на PyPI, исходники — на GitHub. Ставится одной строчкой:

pip install aiogram-dialog-yaml[aiogram]

В двух словах: ты пишешь диалоги в YAML, библиотека резолвит их в готовые словари, которые ты скармливаешь aiogram’у. Тексты, кнопки, медиа, условия — всё в одном файле. Логику можно вставлять прямо в YAML через функции-билдеры. Сверху — опциональная очередь с rate-лимитом, чтобы ты не вылетал с RetryAfter.

Звучит абстрактно. Сейчас на пальцах.

Хеллоу-ворлд

Вот так выглядит самый базовый диалог:

# dialogs.yaml constants: service_name: "Acme Bot" support_link: "https://t.me/acme_support"

welcome_text: | 👋 Привет, {user_name}! Добро пожаловать в {service_name}. Что-то пошло не так? Напиши в {support_link}.

welcome_keyboard: func: static_inline_keyboard data: buttons: - { line: 1, text: “🚀 Начать”, callback: “start” } - { line: 2, text: “💬 Поддержка”, url: “@support_link” }

dialogs: welcome: send_message: text: “@welcome_text” parse_mode: html reply_markup: “@welcome_keyboard”

А вот так — Python:

import asyncio from aiogram import Bot

from aiogram_dialog_yaml import DialogProvider, FunctionRegistry, default_functions from aiogram_dialog_yaml.functions import aiogram_functions

registry = FunctionRegistry(default_functions()) registry.register_many(aiogram_functions())

provider = DialogProvider(“dialogs.yaml”, functions=registry)

async def main() -> None: bot = Bot(token=“…”) section = await provider.get(“welcome”, user_name=“Иван”) await bot.send_message(chat_id=12345, **section[“send_message”])

asyncio.run(main())

Всё. Никакой магии, никаких метаклассов, никаких декораторов. section — это обычный питоновский словарь, который ты прямо в bot.send_message(**section["send_message"]) и пихаешь. Клавиатура — настоящая InlineKeyboardMarkup уже на этом этапе, не строка, не словарь, не «потом сконвертим».

«Окей, — скажешь ты, — это просто шаблонизатор поверх PyYAML. Чё там удивительного?» А вот сейчас будет.

@references и {placeholders} — две стороны одной медали

В YAML есть два способа подставить значение в текст, и они дополняют друг друга.

{name} — это обычный str.format. Куда втыкаешь — туда и подставляется. Простые скаляры: имя юзера, цена, дата.

text: "Привет, {user_name}! Твоя подписка истекает {expires_at}."

@name — это референс на константу или на параметр. Подставляется что угодно — строка, словарь, список, целый сложный объект клавиатуры. И рекурсивно — если значение само содержит {} или @, они тоже разрешатся.

constants: cta_button: func: static_inline_keyboard data: buttons: - { text: "Оплатить ⭐", url: "@payment_link" }

dialogs: paywall: send_message: text: “Подписка истекла, {user_name}. Продли за 199₽.” reply_markup: “@cta_button” # подтянулась вся клавиатура целиком

Зачем два разных синтаксиса? Потому что они решают разные задачи:

{name}

@name

вставляется в строку посередине

да

нет

подставляет сложный объект целиком

нет

да

str.format-совместим

да

нет

можно вложить ссылку на ссылку

нет

да (рекурсия)

Если ты когда-нибудь пытался запихнуть InlineKeyboardMarkup через .format() — ты знаешь, почему пришлось разделить.

Бонус: если переменной нет — не падаем

Самая раздражающая штука у обычного str.format — это что он падает с KeyError, если ты забыл прокинуть параметр. В библиотеке стоит lenient formatter: незнакомый {user_name} просто остаётся в тексте как есть, известные вокруг — резолвятся.

greeting: "Привет, {user_name}! Подписка от {service_name}." await provider.get("greeting", service_name="Acme") # → "Привет, {user_name}! Подписка от Acme."

Не падает. Тебе сразу видно, что забыл прокинуть. Без stacktrace’а на 40 строк.

Кейс №1: «Поменяй кнопку, маркетолог сегодня агрессивный»

Раньше:

# payment_handler.py keyboard = InlineKeyboardMarkup(inline_keyboard=[ [InlineKeyboardButton(text="Оплатить", url=PAY_URL)], [InlineKeyboardButton(text="❓ Помощь", callback_data="help")], ]) # success_paywall.py — другое место, другой импорт keyboard = InlineKeyboardMarkup(...) # ещё семь строк # notifications/renew_reminder.py — третье место, тоже клавиатура keyboard = InlineKeyboardMarkup(...) # ещё семь строк

Маркетолог пишет: «давай эмодзи у «Оплатить» поменяем на ⭐». Я лезу искать. Нахожу пять мест. Меняю четыре, забываю пятое. Пятое — это сценарий, который триггерится раз в две недели у юзеров, у которых истекает подписка по реферальной программе. В понедельник пишут: «у меня кнопка без эмодзи, у друга с эмодзи, чё за дискриминация».

Теперь:

constants: pay_keyboard: func: static_inline_keyboard data: buttons: - { line: 1, text: "⭐ Оплатить", url: "@payment_link" } - { line: 2, text: "❓ Помощь", callback: "help" }

И в любом диалоге, где раньше клавиатура копипастилась — теперь просто reply_markup: "@pay_keyboard". Поменял один раз — поменялось везде.

И, что важно, маркетолог теперь сам может это сделать. YAML — это не Python. Он не упадёт от того, что забыли запятую (упадёт, но при загрузке, с понятной ошибкой про строку и колонку, а не на третий день в проде).

Кейс №2: «А если у нас A/B на разные бренды?»

Реальный сценарий из моей жизни: один и тот же продукт, два бренда — основной (@product_bot) и бюджетный (@product_lite_bot). Разное название сервиса, разные тарифы, разные приветственные тексты, но 80% диалогов идентичны.

Раньше: два бота, два кодбейза, синхронизация через git cherry-pick и слёзы.

Сейчас: одна кодовая база, два YAML-слоя:

provider_main = DialogProvider(["base.yaml", "brand_main.yaml"]) provider_lite = DialogProvider(["base.yaml", "brand_lite.yaml"])

Каждый последующий слой переопределяет предыдущий на уровне секций. Базовые тексты — в base.yaml, бренд-специфика — в brand_main.yaml / brand_lite.yaml. Никакой магии deep-merge, никаких сюрпризов: ты явно видишь, какие секции переопределены.

То же самое работает для локализации: base.ru.yaml + base.en.yaml. Не хочу спорить, какая i18n-схема лучше — это рабочий минимум, который покрывает 95% случаев.

Кейс №3: «Покажи кнопку только премиум-юзерам»

Декларативно прямо в YAML:

main_menu_keyboard: func: static_inline_keyboard data: buttons: - { line: 1, text: "📺 Подключения", callback: "configs" } - { line: 2, text: "💳 Оплата", callback: "pay" } - { line: 3, text: "🎁 Промокод", callback: "promo", if: "@show_promo_button" } - { line: 4, text: "👑 Премиум", callback: "premium_only", if: "@is_premium" } section = await provider.get( "main_menu", show_promo_button=True, is_premium=user.is_premium, )

if: false → кнопка просто не отрендерится. Никаких if’ов в питоне, никаких трёх версий клавиатуры. Это всё та же штука: UI-логика живёт рядом с UI, а не размазана по хендлерам.

А если хочется ветку посложнее — выбрать целую секцию в зависимости от флага — есть встроенный билдер if_else:

welcome: func: if_else data: if: "@show_promo_picture" then: send_photo: photo: "@promo_image" caption: "@welcome_text" reply_markup: "@welcome_keyboard" else: send_message: text: "@welcome_text" reply_markup: "@welcome_keyboard"

То есть: «если у нас сейчас активна промо-картинка — шли с фоткой, иначе — обычным сообщением». Логика в одном месте, оба варианта рядом. Через две недели маркетолог захочет третий вариант — добавляешь, не выкатывая релиз бекенда.

А что если мне нужно что-то совсем кастомное?

Очевидный вопрос: «А если у меня в тексте надо вывести список из БД?» Ну, типа: «Твои активные подписки: Подписка №1 (до 12.06), Подписка №2 (до 30.07), Premium (до конца времён)».

Регистрируешь свою функцию:

async def render_configs(data): configs = data["configs"] return "\n".join( f"{c.name} — до {c.expires:%d.%m.%Y}" for c in configs )

registry.register(“render_configs”, render_configs)

И вызываешь её из YAML:

dialogs: status: send_message: text: func: render_configs data: configs: "@user_configs" parse_mode: html section = await provider.get("status", user_configs=user.configs)

Функция может быть sync или async — библиотеке плевать, она проверит inspect.iscoroutinefunction и сделает правильное.

И вот теперь главное:

Это и есть тот самый «стандарт», к которому всё движется. Декларативно описать что нужно сделать, и иметь дырки для как — Python-функции, которые делают грязную работу. Kubernetes ровно так и устроен: манифесты — YAML, операторы — Go. GitHub Actions — то же самое: workflow в YAML, actions — TypeScript/Docker. Terraform — HCL, провайдеры — Go. Декларативное снаружи, императивное внутри. Это эталон. И в боте я могу так же.

Очередь сообщений и лимиты Telegram

Окей, диалоги — это полдела. Вторая половина — доставка.

Если ты когда-нибудь делал рассылку из бота на 10к юзеров, ты знаешь:

лимит

значение

глобальный исходящий рейт на одного бота

~30 сообщений / сек

сообщения в один групповой чат

~20 / минута

сообщения в один личный чат

~1 / сек (soft, бёрсты ок)

Превысил — получил 429 Too Many Requests с заголовком Retry-After: N. aiogram это превращает в TelegramRetryAfter. Если ты этого не обработал — у тебя половина юзеров не получила сообщения, и теперь иди объясняй CTO, что «ну вот мы упёрлись в лимит Telegram». Эпично.

В библиотеке этим занимается MessageQueue — опциональная (!) штука, которую ты вкомпиливаешь, когда созрел до production:

from aiogram_dialog_yaml.delivery import DialogApplier, MessageQueue

queue = MessageQueue(bot, rate_limit=30) applier = DialogApplier(provider, bot, queue=queue)

await queue.start()

Обычная отправка — едет через очередь, дренируется со скоростью ≤30/сек.

await applier.apply(chat_id=123, section_key=“welcome”, user_name=“Иван”)

Срочное — приоритет 0 пролезает вперёд всех priority-1.

await applier.apply(chat_id=123, section_key=“urgent_alert”, priority=0)

Per-call escape hatch: эту одну отправку — мимо очереди, в обход.

await applier.apply(chat_id=123, section_key=“status”, immediately=True)

await queue.stop(drain=True) # на завершении ждём, пока очередь сольётся

Что внутри:

  • asyncio.PriorityQueue, ключ — (priority, seq). seq — это монотонный счётчик через itertools.count, чтобы при равных приоритетах сохранять FIFO.

  • Sliding window из таймстемпов за последнюю секунду. Превысили rate_limitasyncio.sleep на точную дельту, чтобы попасть в окно. Без time.sleep, без блокировок, без CPU-burn.

  • Воркер не умирает на ошибках. Если конкретная отправка упала — вызывается твой on_error(task, exc), очередь едет дальше. Хочешь ретраи — пиши логику в on_error, библиотека намеренно их не делает за тебя (бо «правильного» ретрая в Telegram нет, всё зависит от твоего сценария).

И самое важное: очередь — не часть ядра. Не нужна — не импортируй. Импортировал — но в этом конкретном месте не нужна — передаёшь immediately=True. Импортировал и нужна почти везде, но иногда надо мимо — без проблем. Три уровня отказа от неё:

хочется

что инстанцируешь

только резолвить YAML, слать руками

DialogProvider

резолвить и сразу слать, без рейта

DialogApplier(provider, bot)

резолвить + очередь + рейт + приоритеты

DialogApplier(provider, bot, queue=...)

Адаптер: «aiogram хочет красивые типы, YAML хочет строки»

Telegram-API имеет пару капризных моментов:

  • send_video_note не принимает URL — надо реально загрузить файл. В aiogram это решается через URLInputFile.

  • send_poll в aiogram ≥3.7 хочет InputPollOption, а не строки.

Не пихать же это в YAML, правда? В библиотеке есть params_adapter — функция, которая запускается прямо перед bot.<method>(...). Ты в неё описываешь нужные трансформации один раз:

def params_adapter(method, params): if method == "send_video_note" and isinstance(params.get("video_note"), str): params["video_note"] = URLInputFile(params["video_note"], "vn.mp4") if method == "send_poll": params["options"] = [InputPollOption(text=o) for o in params.get("options", [])] return params

queue = MessageQueue(bot, rate_limit=30, params_adapter=params_adapter)

YAML остаётся чистым. Бот работает. Все довольны.

Поток выполнения, на одной схеме

Чтобы было совсем понятно, как оно всё устроено

Чтобы было совсем понятно, как оно всё устроено

Каждый слой можно выкинуть. Хочешь только провайдер — окей, выкинул всё остальное, разруливай отправку сам. Хочешь applier без очереди — окей, шлёт напрямую. Хочешь всё — пожалуйста.

Полный демо-бот, который реально живёт

Чтобы это не звучало как «ну там в теории всё круто», в репозитории лежит examples/full_bot/ — настоящий aiogram-бот, который покрывает все практически полезные методы отправки:

send_message, send_photo, send_video, send_audio, send_document, send_animation, send_voice, send_video_note, send_media_group, send_location, send_venue, send_contact, send_poll (обычный и квиз), send_dice, send_sticker, send_chat_action.

Всё это — один dialogs.yaml и сто строк bot.py, бóльшая часть которых — это lifecycle очереди и регистрация хендлеров. Никаких сообщений руками в Python.

Запускается так:

git clone https://github.com/notwizzard/aiogram-dialog-yaml cd aiogram-dialog-yaml pip install -e ".[aiogram]" python-dotenv cp examples/full_bot/.env.example examples/full_bot/.env # вставь токен от @BotFather в .env python examples/full_bot/bot.py

И в Telegram пишешь боту /start — открывается меню из 17 кнопок, и по каждой кнопке прилетает соответствующий тип сообщения. Гифки от Giphy, картинки от Lorem Picsum, видео — сэмпл от Google. Всё бесплатно, ничего регистрировать не надо.

Что под капотом, если совсем коротко

  • Чистое ядро: только PyYAML, ничего больше. Хочешь использовать механизм рендера YAML в чём-то, не связанном с Telegram — пожалуйста.

  • Опциональный aiogram-extra: pip install aiogram-dialog-yaml[aiogram] подтягивает aiogram и включает билдеры клавиатур и медиа.

  • Опциональный delivery-слой: MessageQueue и DialogApplier. Импортируешь только если нужно.

  • Type hints + py.typed: всё типизировано, mypy довольный.

  • 19 pytest-кейсов: приоритеты, FIFO, rate-limit, error handling, layered configs, lenient placeholders, async builders — всё покрыто.

  • CI на GitHub Actions для Python 3.9–3.13.

  • Publish через OIDC Trusted Publishing: я ничего не храню — релиз пушится прямо из GitHub-релиза, PyPI верит мне по подписи.

А зачем вообще ещё одна либа, когда есть aiogram-dialog?

Хороший вопрос, я сам его себе задавал. Есть aiogram-dialog от Tishka17 — мощная штука для FSM-сценариев с окнами и состояниями. Но её фокус — многошаговые диалоги с переходами (типа онбординг-флоу или wizard). А моя боль была про single-shot сообщения: пользователь нажал кнопку — мы ему шлём заранее заготовленный текст с картинкой и клавиатурой. Без состояний. Просто «вытащи мне эту реплику из YAML и отправь».

То есть это дополняющая история, не конкурирующая. У них разные сценарии использования. Можно использовать aiogram-dialog для FSM-флоу и aiogram-dialog-yaml для всего остального копирайта. Я так и делаю.

Резюме «до и после»

ДО

ПОСЛЕ

🗄 Где живёт текст

payment_handler.py, success_paywall.py, notifications/*, admin_commands.py, utils/broadcast.py, texts.py6 файлов

dialogs.yaml1 файл

⌨️ Где живут клавиатуры

копипастятся в каждом хендлере, 7 разных способов сборки

в секции constants: того же YAML, 1 способ

🔍 «Поправь запятую»

grep по проекту, найти 5–7 мест, не забыть ни одно

поменять одну строку в YAML

🌐 Бренды / локали

отдельный кодбейз, git cherry-pick и слёзы

дополнительный YAML-слой через DialogProvider([base, overlay])

👑 Премиум-кнопка

if user.is_premium: в трёх хендлерах

if: "@is_premium" прямо в YAML

🚦 Рассылка на 10к юзеров

TelegramRetryAfter, половина не получает

MessageQueue(rate_limit=30), всё едет

Запятая теперь правится в одном месте за десять секунд. Маркетолог сам залазит и правит, не присылая в личку «слушай, можешь…». Бренд меняется добавлением одного YAML-слоя. Кнопки скрываются/показываются прямо из YAML. Очередь держит Telegram-лимиты, премиум-кнопка показывается только премиум-юзерам, и я наконец могу спать.

Где брать

Лицензия — MIT, делайте что хотите. Issues и PR приветствуются — особенно если у тебя есть свои «кейсы из жизни», которые сейчас в YAML-DSL не закрываются. У меня сейчас 0.1.0, я открыт ко всему.

P.S.

Если ты дочитал до сюда — спасибо. Если у тебя есть свой Telegram-бот и ты тоже задолбался искать запятую — попробуй, мне будет интересен фидбек. Если ты пилишь что-то другое, и эта статья тебе не пригодилась — ну, по крайней мере, ты теперь знаешь, что про Telegram-API-лимиты надо думать до того, как делаешь рассылку на 10к юзеров.

Не делайте ботам больно. Особенно в пятницу вечером.