Почему не стоит запихивать хендлеры внутрь других хендлеров
Почему нельзя делать так?
@dp.message_handler(commands=['weather'])
async def weather_handler(message: types.Message):
await bot.send_message(message.chat.id, text='Введи название города...')
@dp.message_handler()
async def get_city_name(message: types.Message):
# обработка введённых данных от юзера
pass
PS. Табуляция верна.
PS_2. Сделал вопрос чтобы на него ссылаться в будущем.
Ответы (1 шт):
Ответ в большинстве своем подходит как для aiogram так и для pytelegrambotapi.
Для начала опишу примерный ход действий бота при получении сообщения.
- Вы запускаете бота.
- Он проходит по всему видимому коду, и записывает(видимые) хендлеры в свой внутренний список.
- Юзер отправляет команду
/weather - Бот проходит по списку хендлеров, и начинает сверяться с фильтрами внутри хендлеров, по типу
content_types=[...],regex="...", ну или некую лябмюдуfunc=lambda msg: msg.text=='...' - Натыкается на
commands=['weather']и тригерит функциюweather_handler - И тут начинается хрень. Программа проходит по коду
weather_handlerи видит еще один хендлер, который не имеет фильтра (по факту будет иметьcontent_types=['text']иstate=None(Aiogram)), и само собой записывает его в свой список хендлеров (смотри пункт 2). - Дальше юзер вводит текст, который с большей долей вероятности попадает в
get_city_nameи вроде бы код даже работает как надо, и в целом можно забить на эту тему, при условии что ваш бот имеет только одну такую шляпу.
Проблемы возникают когда появляется еще одна функция без фильтров.
К примеру такой код:
@dp.message_handler(commands=['weather'])
async def weather_handler(message: types.Message):
await bot.send_message(message.chat.id, text='Введи название города...')
@dp.message_handler()
async def get_city_name(message: types.Message):
# обработка введённых данных от юзера
pass
@dp.message_handler()
async def get_all_text(message: types.Message):
pass
Будет всё ломать. Функция get_city_name никогда не сработает, так как в список хендлеров попадет позже чем get_all_text и вы будете грустить, что не продумали это заранее.
Итак, как это исправить, чтобы это работало наверняка?
pytelegrambotapi
Для pytelegrambotapi нужно юзать register_next_step_handler.
@bot.message_handler(commands=['weather'])
def weather_handler(message: types.Message):
bot.send_message(message.chat.id, text='Введи название города...')
bot.register_next_step_handler(message, get_city_name)
def get_city_name(message: types.Message):
# обработка введённых данных от юзера
pass
@bot.message_handler()
def get_all_text(message: types.Message):
pass
Логика работы следующая:
в register_next_step_handler пихаете первым аргументом объект Message который в целом можно брать текущий без создания нового по типу
@bot.message_handler(commands=['weather'])
def weather_handler(message: types.Message):
msg = bot.send_message(message.chat.id, text='Введи название города...')
bot.register_next_step_handler(msg, get_city_name)
Видел как так делают, но особого смысла нет. Разве что вы пересылаете месседж в какой-то другой чат, то тогда стоит делать именно так.
А другим аргументом название функции, которая должна сработать. Само собой в той функции должен быть обязательный аргумент message: types.Message иначе будет ошибка.
Также вы можете передавать аргументы в следующую функцию. К примеру так.
@bot.message_handler(commands=['weather'])
def weather_handler(message: types.Message):
bot.send_message(message.chat.id, text='Введи название города...')
bot.register_next_step_handler(message, get_city_name)
def get_city_name(message: types.Message):
bot.send_message(message.chat.id, "Введи что-то еще")
bot.register_next_step_handler(message, get_extra_data, message.text)
def get_extra_data(message: types.Message, city_name: str):
print(city_name, message.text)
Тут мы в еще одну функцию передаем то что юзер написал в текущей(название города), а в следующей можно получить еще какие-то данные и обработать всё как вам нужно.
Ну и напоследок скажу, что аргументов так можно передать сколько вы хотите, просто добавляйте их через запятую.
Возможные проблемы
Ну во первых нельзя красиво сделать так чтобы прервать этот register_next_step_handler.
То есть если вы вдруг захотите во время получения названия города прекратить это, то вам придется либо дойти до конца цепочки, либо в каждой такой функции писать что-то типа
def get_city_name(message: types.Message):
if message.text == "/cancel":
bot.send_message(message.chat.id, "Вы прервали ввод")
return
bot.send_message(message.chat.id, "Введи что-то еще")
bot.register_next_step_handler(message, get_extra_data, message.text)
Что выглядит довольно убого.
Aiogram
Для aiogram стоит использовать state. Пример.
from aiogram.contrib.fsm_storage.memory import MemoryStorage
from aiogram.dispatcher import FSMContext
from aiogram.dispatcher.filters.state import StatesGroup, State
bot = Bot(token="token")
dp = Dispatcher(bot, storage=MemoryStorage())
class WeatherStates(StatesGroup):
get_city_name = State()
get_extra_data = State()
@dp.message_handler(commands=['weather'])
async def weather_handler(message: types.Message):
await bot.send_message(message.chat.id, text='Введите название города...')
await WeatherStates.get_city_name.set()
@dp.message_handler(state=WeatherStates.get_city_name)
async def get_city_name(message: types.Message, state: FSMContext):
await bot.send_message(message.chat.id, text='Введите что-то еще...')
await WeatherStates.next()
async with state.proxy() as data:
data['city_name'] = message.text
@dp.message_handler(state=WeatherStates.get_extra_data)
async def get_extra_data(message: types.Message, state: FSMContext):
async with state.proxy() as data:
print(data['city_name'], message.text)
await state.finish()
Выглядит длиннее, но могу уверять что значительно надёжнее. Что мы тут делаем, импортируем нужные модули, и после делаем диспатчер с аргументом storage=MemoryStorage() нужно для того чтобы боту было где хранить данные стейтов и просто нужные данные.
Дальше делаем класс для стейтов.
После мы вместо register_next_step_handler пишем await WeatherStates.get_city_name.set() как-бы запускаем конкретное состояние для определённого юзера(а еще для определенного чата).
После юзер пишет название города и попадает в get_city_name где мы сразу меняем стейт await WeatherStates.next() на следующий в группе WeatherStates и записываем то что написал юзер в словарь.
async with state.proxy() as data:
data['city_name'] = message.text
Примерная схема хранения такая:
память_стейта = {
"какой-то_чат_айди": {
"какой-то_юзер_айди": {
"state": "",
"data": {}# тут хранятся данные
}
}
}
Ну а потом юзер снова что-то пишет мы попадаем в get_extra_data и снова заходим в хранилище и оперируем данными, как мы хотим.
И напоследок await state.finish() сбрасывает текущий стейт, и все данные в хранилище для чата->юзера.
Ну это нужно делать тогда когда вам точно не нужны данные в хранилище стейта, и сам стейт. Можно конечно сбросить только данные так await state.reset_data() или только стейт await state.reset_state(with_data=False)