Фильтр гарантирует
HasText() пропускает только сообщения с текстом. Фото без подписи до handle() не дойдёт.
selfrotgram · Telegram Bot API для Python
Если хендлер обещает, что у сообщения есть текст, фильтр это проверил, а message.text в редакторе — str, а не str | None. Без assert, без cast, без скрытых аргументов.
Альфа: 0.1.1–0.1.4 на PyPI, сентябрь 2026. Работает на реальном боте — chestor_bot.
class Echo(MessageHandler[BaseContext[TextMessage]]): # обещаю: текст есть
query = HasText() # проверяю: текст есть
async def handle(self) -> None:
# text: str — проверять нечего
await self.ctx.message.answer(self.ctx.message.text)$ python -m bot Запущен @echo_bot, жду апдейты (long polling)
01 · Что гарантирует фильтр
Нажми «Забыть HasText()» в примере выше: библиотека не даст запуститься боту, который врёт о своих типах.
HasText() пропускает только сообщения с текстом. Фото без подписи до handle() не дойдёт.
TextMessage в MessageHandler[…] — суженный тип: у него text: str. Таких типов 117, по одному на каждое необязательное поле.
Обещание и фильтр сравниваются при импорте. Разошлись — DefinitionError при запуске, а не AttributeError у пользователя.
02 · Было / стало
Главное отличие в подходе: aiogram опирается на функции и неявную передачу аргументов, selfrotgram — на классы и явный ctx.
@router.message(Command("sum"))
async def sum_handler(message: Message, command: CommandObject):
if not command.args or len(command.args.split()) != 2:
await message.answer("Использование: /sum 2 3")
return
a, b = map(int, command.args.split())
await message.answer(f"{a + b}")class Sum(MessageHandler[BaseContext[TextMessage]]):
cmd = Command("sum", args_count=2)
query = cmd
async def handle(self):
a, b = (int(x) for x in self.cmd.parse(self.ctx).args)
await self.ctx.message.answer(f"{a + b}")Команда с другим числом аргументов просто не подходит — проверять руками нечего.
Все пары и честный список отличий — в docs/from-aiogram.md.
03 · CLI
tree рисует дерево роутеров и находит недостижимые хендлеры. check собирает все ошибки описания разом — для CI, с --strict.
$ $ selfrot tree Dispatcher (мидлвари: LoggingMiddleware) └─ RootRouter ├─ StartRouter │ └─ Start message: TextMessage Command('start') └─ AdminRouter (мидлвари: OnlyAdmins) ├─ BanUser message: TextMessage (FromUser(1, 2) & Command('ban', Ban)) ├─ Anything message без фильтра: ловит всё этого вида ├─ Never message: TextMessage HasText() ! недостижим: выше Anything ... └─ Joined chat_member: ChatMemberUpdated MemberJoined() Роутеров: 3 (без корня), хендлеров: 4. allowed_updates: chat_member, message
04 · Внутри
05 · Установка
Ставится как selfrotgram, импортируется как selfrot. Зависимости совместимы с aiogram 3.x — переезжать можно постепенно.
$ pip install selfrotgram$ poetry add selfrotgram$ selfrot init