Мои первые шаги
Создано специально для телеграм канала https://t.me/debug_u
Как я учусь делать телеграм бота?
Вступление
Приветствую всех, кто решил прочесть эту статью. В ней я постараюсь изложить все этапы создания бота, все материалы, что я прочитал, все ошибки, которые я совершил и осознал, в общем весь путь. Мое решение может быть плохим, так что я приветствую критику и предложения от более опытных и мудрых людей. Если хотите больше информации о моей деятельности, переходите в телеграм канал Debug_Yourself.
В чем идея бота?
Перед тем, как выбрать идею для бота, я понял, что лучше не пытаться сразу сделать "сложного" бота со сверхбогатым функционалом, потому что сложность на начальном этапе, как правило, всегда убивает энтузиазм и желание заниматься выбранным делом в принципе. Также я хотел сделать что-то более менее пригодное и актуальное, что позволило бы мне освоить весь базовый функционал, необходимый для реальных проектов. Скажем, можно было сделать криптобота, который бы позволял получать цену в $ интересующей крипты. И на самом деле я такой сделал - это слишком просто. Уверенности я в себе не почувствовал уж точно. Такой бот в чистом виде неактуален, хотя врать не буду, я поработал с API коинмаркеткапа, что уже неплохо.
Я люблю выпить ягодный чай в кофейне, где кофе на вынос. Но свой заказ приходится ждать, неважно сколько, иногда время есть только на то, чтобы забежать, сразу взять свой чай и по делам. Но заказы онлайн они не принимают. И сразу идея - сделать бота для онлайн заказов. Это автоматизация бизнеса, что актуально. Это также тот базовый функционал, которые имеют большинство ботов, и более того, это возможно по готовности предлагать владельцам купить такого бота.
В общем решено - делаю бота для онлайн заказа в кофейне
На чем я буду делать бота?
Разумеется, это никакой не конструктор. Я уже писал у себя на канале, почему лучше изучить прогу и написать бота, чем пользоваться конструкторами. Если коротко, то
Конструктор обычно решает шаблонные задачи, нетипичные проблемы всегда требует незаурядного ума.
Так как я немного знаю язык программирования (ЯП) Python, то и писать его буду на нем. Про достоинства этого языка можно написать отдельный пост (ну или прочитать в инете), поэтому сильно его нахваливать не буду. Лишь скажу, что в целом вы можете писать тг ботов на:
PHP
Java
nodejs
Python
C#
Go
еще парочка других
Полный список можете глянуть по ссылке. Там же вы увидите, что у популярных языков есть несколько библиотек. Python в частности имеет
pyTelegramBotAPI (почему-то не указана в этом списке)
Какая библиотека лучше? Разумеется, лично я сказать не могу, так как не имел опыта работы с ними. С другими тоже особо не говорил по этому поводу. Если судить по звездам на гитхабе, то python-telegram-bot обходит остальные. Потом идет Telepot и замыкает тройку pyTelegramBotAPI. Тем не менее я пользуюсь последней. Во многих обучающих примерах использовали именно ее, плюс она показалась довольно простой и привлекательной.
Осталось упомянуть базу данных и используемую среду разработки. Я сразу понял, что без БД никуда, ну точнее можно без нее реализовать бота, но в реальных проектах вряд ли боты обходятся без БД, поэтому я решил ее использовать. MySQL/PostgreSQL я не знаю (за что даже стыдно), был опыт работы с SQLite, правда не в чистом виде, а когда изучал фреймворк Django (об этом как-нибудь потом), но там используется ORM. Если не знаете что такое ORM, могу порекомендовать быстро прочесть тут, мне показалось довольно хорошим такое объяснение.... Возвращаясь к БД, я решил использовать MongoDB. У меня был небольшой опыт работы с ней, когда изучал Node.js, она мне сразу понравилась, и мне показалось, что она вполне годится для моего бота.
В качестве среды разработки я использую Visual Studio Code. Опять же сред много, вы найдете свою только когда попробуете несколько.
Хотя я бы новичкам рекомендовал PyCharm - не ошибетесь.
Первые шаги
Разумеется, изначально я посмотрел документацию выбранной библиотеки - это надо стараться делать первым делом, когда беретесь изучать что-то новое. Но и это правило имеет исключения. По моему мнению хорошая документацию должна содержать не только список имеющихся в ней "вещей", а непосредственно примеры их использования. Когда автор описывает способы реализации своих функций и дает к ним пояснения, он показывает свое видение. Тебе, как ученику, становится легче проследить его логику и понять надобность. Я читал документацию по Django и мне она понравилась. На тебя не валят сразу гору сухой информации, напротив, новичкам предлагают создать просто веб-приложение, где поясняется смысл почти каждой команды. Ты поэтапно собираешь пазлы, получая в конце всю картину. А уже после тебе дают более углубленные знания и эффективные приемы.
Так вот касаемо библиотеки pyTelegramBotAPI - я не в восторге. Все что в ней описано - понятно, но не описываются нюансы, и ты не до конца понимаешь, как она себе поведет в той или иной ситуации. Конечно, есть вероятность, что виною всему моя неопытность. Тем не менее из всех остальных библиотек на python, эта показалась самой доступной.
КОГДА УЖЕ ПРИСТУПИМ К СОЗДАНИЮ БОТА???
Можно выдохнуть, уже! Условимся, что у вас установлена понравившаяся вам среда разработки, есть python. Причем python3. Версию можете ставить самую последнюю, на момент написания это python 3.7.
Теперь надо создать бота. Для этого в тг существую "крестный отец" - с ником @BotFather. Жмете /start
, потом /newbot
, вводим имя нашего будущего бота, вводим его ник типа @something_bot - в общем следуем инструкции. В случае успеха он пришлет нам token бота типа Use this token to access the HTTP API: 431603025:AAGH3fmgfX6C_AuoiynFuMUgfsd9oPDzSo
Именно с токеном мы и будем работать.
Никому не показывайте свой TOKEN, в противном случае вашим ботом смогут управлять третьи лица
У нас есть бот, можем кодить..... А вот тут еще одна загвоздка. У граждан гулагской федерации возникают трудности, ибо телеграм запрещен. При обращении к серверам телеграма, у меня начали возникать ошибки. Я понял, что сейчас VPN нужен как никогда. Не сказать, что я бедный, но платный брать не хотелось, по крайней мере на момент обучения. Среди бесплатных много говна. Но в итоге мне удалось найти хороший VPN, правда пришлось чуточку попотеть.
В одном из чатов добрые люди просто скинули ссылку https://www.freeopenvpn.org/connect.php и сказали следовать инструкции (она на сайте подробно описана, также я для своих подписчиков на всякий случай описал свою инструкцию).
Скачать прогу в зависимости от вашей ОС
Далее выбираем понравившийся сервер
Качаем файл с настройками.
Вводим логин freeopenvpn и пароль, которые меняется раза два в сутки вроде
Скорость сильно не падает, зато теперь я смог запускать бота, и он работал.
Кстати, я не уточнил, сам я использую macOS, винду не очень люблю и вам не советую. Ставьте linux. Либо второй системой рядом с виндой, либо на виртуальной машине. Расписывать про выбор дистрибутива и его установку смысла не вижу, по крайней мере тут. Если будет желание и необходимость, отдельно сделаю. Сам я с линуксом мало работал, плюс начинал знакомство сразу с довольно хардкорного фреймфорка - Arch Linux.
Начинаем кодить
Когда я изучал веб-программирование, в одном уроке хороший человек сказал, что в проектах надо использовать виртуальное окружение. Потому что у каждой библиотеки есть свои версии, иногда новые версии несовместимы со старыми, отсюда могут возникать ошибки. Банальный пример с самим питоном: у вас несколько проектов, один из которых на втором питоне, другой на третьем. А эти версии без обратной совместимости! Помимо этого вы очень трудолюбивый и имеет еще три проекта на Django, где у одного версия 1.10, у второго 2.0 и третьего 1.8.
Именно в таком случае при создании каждого проекта создается виртуальное окружение, изолируя проект от вне. Это позволяет вам использовать в каждом проекте именно необходимые вам версии библиотек.
Таким образом вы устанавливаете все требуемые библиотеки, сохраняете их версии в специальном файле requirements.txt, и когда кто-то захочет воспользоваться вашим творением, ему достаточно будет установить все из этого файла. Подробную инфу можете глянуть здесь, либо посмотреть это видео.
Напомню, я пишу на mocOS, но команды также будут актуальны (не все) и для Linux. Оконщикам придется перебираться на другую систему (но вам будет полезно, обещаю).
Виртуальная среда у меня уже была, но вообще она устанавливается так
pip3 - это менеджер пакетов для python, при чем для python 3 будет pip3, для python 2 просто pip. virtualenv - это название пакета виртуального окружения
Итак, я создал каталог на рабочем столе gh_coffee_bot. Открыв его в терминале, я ввожу команду
venv - это мое название для созданного виртуального окружения.
Внутри gh_coffee_bot появился каталог venv.
Также я создал внутри каталога gh_coffee_bot еще один с названием app. В нем я храню файлы с ботом.
Но установить вирт.окружение мало, надо его активировать. В каталоге gh_coffee_bot в терминале достаточно прописать
и оно станет активным, понять это можно по добавленному (venv) слева строки терминала.
Также можно посмотреть, что уже установлено командой
Вирт.окружение у нас пустое, поэтому там ничего не будет.
Далее я установил саму библиотеку pyTelegramBotAPI
Создал в каталоге app файл bot.py, это будет главный исполняемый файл. В нем сначала импортировал все требуемые модули и попробовал написать просто бота, который бы отвечал нам на любое наше сообщение, дабы удостовериться, что все будет работать дальше.
Не знаю, стоит ли пояснять все детально. Здесь просто идет импорт библиотеки на 1 строчке, на 3 я создал экземпляр класса TeleBot(), куда передал свой токен в виде строки, а с 5-7 строки начинают творить магию, о ней будет ниже. Могу посоветовать прочитать официальную документацию и повторить все примеры.
Код выше работал, а значит можно уже задуматься над какой-то структурой, ибо мешать все в один файл - это гиблое дело.
Во-первых, пришла в голову мысль создать файл config.py, куда я отдельно записал свой token, а также пару функций, которые напрямую к боту не относятся (но о функциях позже).
Во-вторых, у меня будет БД, значит надо создать отдельный файл для работы с ней. Пусть пока что мой бот будет просто сохранять всех новых пользователей, а именно их first_name и last_name (возможно, это и бессмысленно, потому что юзеры могут их спокойно менять, но пока что пусть будут). Также обязательно буду сохранять их user_id, он у каждого уникальный в тг, значит по этому параметру мы потом будет к этим юзерам обращаться и получать какие-либо требуемые данные. Не помешает сохранять время "регистрации" пользователям, то есть когда он добавляется в БД. И также я добавлю пользователю еще один параметр state, об этом я опишу чуть позже.
Для работы с MongoDB я разумеется установил саму mongo (команда актуальна для macOS)
Установил пакет pymongo.
Также я скачал программу Robo 3T, чтобы можно было визуально работать с базой данных. Чтобы запустить установленную БД, ввел
Создал файл db_users.py (это все в каталоге app), где подключился к БД и написал три функции
Логика довольно простая: check_and_add_user(message)
проверяет, есть ли пользователь в нашей БД по его id, который получаем при помощи message.from_user.id, get_current_state(user_id)
позволяет нам получить текущее состояние пользователя (значение параметра state), ну и при помощи set_state()
мы устанавливаем пользователю новое состояние. На самом деле с "состояниями" я немного поторопился. Зачем они вообще нужны и в чем их логика? Пока что я скажу так - забудьте на время про них вообще, когда я дойду до этапа, на котором я до них додумался, я про них расскажу.
Теперь мы может перейти в наш файл bot.py, и начинать писать что-то серьезное. Хотя не совсем. Я вроде понимаю, что должен делать бот: сначала выбирать общий раздел напитков, потом выбирать конкретный напиток, после выбор размера. После всего в идеале товар добавляется в корзину и так можно выбрать несколько, а потом уже оплатить. Но сколько всего разделов, сколько напитков/сендвичей и прочей продукции. В каком виде вообще хранить эту информацию?
Сначала я задумался о том, что надо все хранить в БД. Но такая задумка меня напугала своей сложностью реализации, поэтому я решил это дело оставить на вторую версию своего бота. Что я сделал? Нашел кофейню, где на сайте есть меню и взял его часть. Пока что полностью оно мне не нужно, мне важно простроить логику, а добавить оставшуюся продукцию всегда можно.
Таким образом я создал еще один файл в каталоге app под названием gh_menu.py. В нем я создал словарь gh_menu, куда добавил часть меню:
Честно говоря я не знаю пока, насколько удачно я придумал это. Время покажет...
Ну теперь то я могу перейти в файл bot.py и начать кодить.
Импортировал все необходимые модули
Создание объекта бота теперь немного изменилось
Командой config.TOKEN
мы обращаемся к нашему модулю config к созданной переменной TOKEN.
Сначала нашего пользователя надо поприветствовать и коротко объяснить суть происходящего. Для этого создал функцию send_welcome(message)
, которая будет вызываться тогда, когда пользователь первый раз зайдет в бота, ну либо введет команду /start
В созданной функции мы как раз таки первым делом проверяем, есть данный пользователь или нет, и если нет, то добавляем его (это 16 строчка). 18-19 строчки отвечают за создание кастомной клавиатуры, в моем случае будет лишь одна кнопка "сделать заказ". На 20 строчке мы вызываем объект bot, у него есть метод send_message()
куда мы первым должны передать id нашего пользователя (некоторые делают это так message.chat.id
, но я считаю правильнее получать именно id пользователя, а не чата), также надо передать текст сообщения, но так как мое сообщение длинное, для эстетического баланса я создал словарь text_messages
, у которого есть два ключа start и help. Так как мы отвечаем на пользовательскую команду /start
, то и вызываем значение по этому ключу. Методом format
я лишь передаю в этот словарь имя нашего пользователя, которое содержится в объекте message. Третий параметр reply_markup опциональный, но так как я создал клавиатуру, то передаю ее reply_markup=markup
.
Вообще в хэндлере @bot.message_handler()
я могу написать разные условия: например, если мне надо, чтобы бот реагировал на команды через /
, типа /start
или /help
, то пропишу @bot.message_handler(commands=['start', 'help'])
, или могу разделить и написать для каждой команды отдельно. Если я хочу, чтобы бот реагировал на текстовое сообщение/эмодзи (а эмодзи, это тоже текст, если я не ошибаюсь), то пропишу @bot.message_handler(content_types=['text'])
, и по аналогии вместо text можно вставлять аудио, документы и прочие объекты тг. Функционал моего бота пока ограничится текстом.
Теперь мне надо мою идею из головы как-то конвертировать в алгоритм хотя бы на бумаге, а в итоге в код. Изначально пользователь видит приветствие бота, читает инструкцию и жмет на кнопку "Сделать заказ". Ему должно показаться меню с выбором категорий, типа
Особые напитки
Кофе
Горячие напитки
При выборе категории пользователю надо показать меню с перечнем конкретных товаров. К примеру, если это будут "Особые напитки", то надо отобразить:
Латте Лаванда Шалфей
Раф Лимонный Пай
Раф Шоколадный трюфель
Кедровый раф
Капучино Соленая карамель
Зеленый капучино
И так уже для каждой категории. При выборе конкретного товара надо отобразить доступные вариации, к примеру, если это напиток, то надо указать размер стаканчика, если это какой-нибудь сэндвич, то надо указать вариации, типа "с лососем" или "с курицей".
Честно говоря, на время написания этого куска статьи я уже попробовал одну реализацию бота, которая потерпела крах. Но я считаю нужным сначала рассказать о ней, чтобы вы смогли увидеть мою логику (или ее отсутствие), посмотреть на ошибки, как я постарался их исправить, а после пока то, что имею на данный момент.
Итак, я понял, что в итоге бот должен отрисовывать пользователю меню, получать какой-то текст, понимать, что требуется пользователь и отрисовывать следующее меню. И так до оплаты. Теперь меньше абстракции:
Этап - пользователь зашел в бота либо нажал
/start
Этап - выбирает категорию
Этап - выбирает конкретный продукт выбранной категории
Этап - Выбирает размер напитка/вид сэндвича и тп
Этап - по идее выбор добавляется в корзину (надо потом в БД создать отдельную переменную и сохранять туда), после чего можно предложить еще что-нибудь, циклично вернув на 2 этап, либо оплата
Оплата
В голову сразу идея приходит - для каждого этапа напишу свою функцию. В итоге файл bot.py
выглядел примерно так:
Что можно видеть? Все, как и описывал выше. Для 2 этапа написал функцию def choose_categories(message)
, для 3 этапа def choose_drink(message)
, и затронул немного 4 этап def choose_size(message)
. Остановился, чтобы проанализировать, что я имею. Еще забыл упомянуть, что наличие хэндлеров подразумевает, что вызовется та функция, которая первая подходит по условию. Конечно, у меня сейчас везде один и тот же хэндлер, что мне уже не нравится, но это стоит учитывать, потому что изначально я этого не сделал.
Для того, чтобы из одной функции вызвать другую, можно использовать register_next_step_handler()
, куда надо передать объект message и, собственно, название самой функции. В принципе, в таком случае вообще можно оставить хэндлер только у функции choose_categories() , а остальные убрать. Но такая концепция подошла бы, если путь в боте был прямолинейный. А ведь пользователь может ошибиться и захотеть вернуться назад, и тут register_next_step_handler()
терпит крах. Что я имею ввиду: допустим юзер уже зашел в категорию, выбирает кофе, но передумал и хочет вернуться опять в "категории". На словах все просто, а как дела с кодом обстоят? Тут небольшое безумие. В боте кнопки кнопками как таковыми не являются, то есть кнопки - это не отдельный объект, это просто удобный для пользователя интерфейс. Мы, как программист, пишем программу, которая умеет работать только с определенным набором команд (вводимым пользователем текстом). И мы эти команды ждем от пользователям, поэтому и создаем кнопки, чтобы пользователь мог, нажав на них, отправить нужный нам текст. Это не кнопки, как в том же веб-программировании.
Это я все пишу для того, чтобы дать понять, что нажав на кнопку, юзер точно также отправляет текст, как если бы он просто его ввел. Ну это наверное и так очевидно. И когда наш пользователь, допустим, находится в функции выбора напитков choose_drink()
и жмет на кнопку назад, чтобы перейти снова к выбору категорий (для нас это функция choose_categories()
) то по факту он все равно сначала попадет в функцию choose_size()
, потому что обработать отправленный текст, насколько я понял, можно только в следующей функции.
И, как можно видеть, я как раз добавил в самом начале choose_size()
условие if
Если полученный текст message.text
равен "назад", то просто вызываю функцию получения категорий. Сначала я пытался это провернуть путем вызова register_next_step_handler()
, но возникал баг: чтобы перейти назад, надо было боту отправить два раза что-нибудь (то есть сначала ты тыкаешь "назад", бот замирает, потом еще раз что-нибудь пишешь, и он отправлял в функцию выбора категорий). Так что и не пытайтесь делать также (разве только в качестве опыта).
Работа над ошибками
Первое, что можно заметить, это повтор кода. Прямое нарушение принципа DRY. В каждой функции я заново прописываю создание объекта кастомной клавиатуры, создаю отдельные кнопки путем markup.row('что-то')
. Разумеется, на ум приходит идея написать функцию, которая бы сама создавала клавиатуру, а мы бы просто туда передавали название кнопок.
Для этого я создал в файле config.py функцию create_menu()
Итак. Функция требует два параметра: mass - это список наших кнопок по типу ['Горячие напитки', 'Кофе', 'Особые напитки']
, back - это булевская переменная, если она будет True, то в наше меню будет добавляться кнопка "Назад". Она нам может быть не всегда нужна, поэтому это вполне логичное решение.
Сразу оговорюсь, что на мой взгляд оптимальным будет сделать клаву в 2 столбца. В 1 слишком широко, в 3 надписи не влезают. А мы все таки хотим, чтобы интерфейс был приятным.
На 7 строке создал объект клавиатуры. Далее начинаю проверять, если массив содержит лишь один элемент, то просто его добавляем и на этом все. Если же >1, то мы переходим в цикл While
, где просто делаем срез полученного списка на 14 строке (первые два элемента), добавляем элементы среза в клавиатуру и удаляем его(это строки 15-16). И так циклично, пока длина массива элементов не станет равной единице. В этом случае сработает условие на 18 строке, добавится последний элемент и произойдет выход из цикла. И на 25 строке произойдет проверка, если back=True
, то в клаву еще добавиться кнопка "Назад".
Конечно, можно еще немного попотеть и сделать более гибкой функцию в плане того, чтобы выбирать количество столбцов в клаве (хотя их можно быть не больше 3х), но я посчитал для себя это лишним гемором.
Теперь хочу приоткрыть тайну загадочного поля "state" у наших юзеров. Вот так сейчас выглядит юзер (это я) в БД
И хотя я могу называть юзеров объектами, с точки зрения самой БД наверное правильно называть их документами. MongoDB является документоориентированной БД. И при работе с ней вы можете каждый свой "объект" сохранять в виде документа. Документы хранятся в коллекциях. В моей случае документы юзеров хранятся в коллекции users. Не буду сильно углубляться в структуру Монги, прочтите сами и поймете, о чем я говорю.
Так что же за state, показанный на скрине "Выбор категории"? Для того, чтобы в процессе работы бота вызывать требуемые нам функции, надо иметь какой-то параметр, в зависимости от которого они и буду вызываться. То есть я создал у юзера поле state, и теперь могу присвоить значение "Выбор категории" и написать функцию, которая будет вызываться только тогда, когда значение state="Выбор категории"
.
Другими словами, функции в боте будут вызываться в зависимости от состояния. Ну и логично, что у каждого состояния должна быть своя функция
Теперь хочу снова обратить внимание на файл db_users.py
Тут я и работаю с состояниями. Я даже еще раз повторю логику функционала. А Логика довольно простая: check_and_add_user(message)
проверяет, есть ли пользователь в нашей БД по его id, который получаем при помощи message.from_user.id. Если пользователя нет, то добавляем new_user. get_current_state(user_id)
позволяет нам получить текущее состояние пользователя (значение параметра state), ну и при помощи set_state()
мы устанавливаем пользователю новое состояние.
Сами состояния я пока храню в виде констант в файле config.py
по типу:
Это еще не весь список. Дело в том, что меня пугает такое большое количество состояний. И знаете почему? Потому что я писал выше, что на каждое состояние требуется своя функция. И получается их довольно много, одни скорее всего однотипные. И я пока не придумал, как уменьшить код.
Ну хорошо, теперь стоит показать вам как преобразился файл bot.py
Поясню некоторые моменты, начиная сверху.
В функции send_welcome()
добавилась строка 30, где я из модуля db_users
вызываю функцию set_state
чтобы установить новое состояние, следуя философии, описанной выше. Вызов этой функции вы можете наблюдать в конце каждой функции файла bot.py
либо условия if
, ну это логично. Как только функция отработала, нам надо установить новое состояние пользователя и исходя из него будет вызвана следующая функция. Но как именно? А вы обратили внимание, что теперь у каждой функции поменялся хэндлер (к примеру строка 33). Вот как он сейчас выглядит
Теперь в хэндреле происходит получение текущего состояния пользователя из БД путем вызова функции get_current_state()
в модуле db_users
. И если полученное состояние равно необходимому нам, в данном случае состояние вызова категорий, полученное из модуля (файла) config.py
.
По такой логике я решил сделать всего бота. Преимущество в том, что теперь легко управлять тем, когда и что будет вызываться. Но количество кода вырастает в несколько раз. Я думаю все дело в том, что и бот и библиотека в целом больше функциональные. Нету ООПа. Невозможно работать с абстрактными объектами. Возможно, мне придет в голову решение, как организовать лучше. Но пока будет так.
Также я бы обратил ваше внимание на строку 40, где я создаю меню при помощи написанной мной же функции create_menu()
, и туда передаю список, так сказать, слов (строк), которые в меню должны отображаться. И я не создаю новый список с этими строками, а делаю волшебство на строке 39. Ведь у нас есть файл gh_menu.py
, в котором создан одноименный словарь gh_menu. Я просто получил все его ключи. Опять же преимущество в том, что при правках в меню, мне стоит поменять лишь словарь gh_menu, при этом не беспокоясь о клавиатуре.
По ходу написания кода я понял одну вещь: вот мы в функции get_categories()
показали пользователю меню категорий. Каждая категория подразумевают собой еще функцию с отрисовкой другого меню для пользователя. Но как из первой функции вызвать вторую, причем надо вызвать нужную, то есть при выборе "особых напитков" к примеру надо вызвать get_special_drinks()
и тп. Для этого пришлось написать еще одну функцию choose_categories()
на строке 47, прокоментить как промежуточную функцию, ведь в ней по факту просто происходит условие выбора. Такой же промежуточной функцией является и choose_good()
(то есть выбор товара) на 102 строке, где точно также происходит выбор товара.
И обратите внимание, что там реализация кнопки "Назад", которая работает адекватно.
То есть если из какой-либо из функций выбора товара пользователь захотел вернуться назад к категориям, то обработка происходит именно тут. Я на 2 строке перезаписываю состояние пользователя в БД и на 3 строке вызываю функцию категорий.
Больше в принципе пояснять нечего. Вроде все объяснил. Но чтобы вы почувствовали мое беспокойство, вспомните еще раз мои слова о том, что каждый компонент это бота, будь то список категорий, конкретная категория, список товаров или конкретный товар - для всего своя функция. А теперь прикиньте сколько имеется товара. И для каждого своя функция! При этом они однотипны. Надо сделать какую-то абстракцию, чтобы не штамповать 100500 функций.
Щепотка абстракции
Немного передохнув, я вновь сел созидать. Но в процессе отдыха я понял, что совершаю еще несколько ошибок.
Во-первых, я слишком сильно заострил внимание на содержании бота. Какой смысл было добавлять столько товара в каждую категорию в файле gh_menu? Ведь моя задача на данном этапе заключается, грубо говоря, в проверке гипотезы - нужен ли этот бот людям, в частности кофейням. Исходя из этого я немного подредактировал файл gh_menu.py
, просто оставив везде по два товара
Во-вторых, я слишком париался по поводу "неидеальности" кода. Да, писать только на "говне и костылях" не лучший вариант. Но и программирование через призму перфекционизма, опять же, на данном этапе лишнее. Пусть где-то много кода. Его наверное всегда будет много. И всегда можно сделать эффективнее и короче. Но опять же - на данном этапе надо делать как знаешь. Вот когда я покажу прототип кофейням, кто-то заинтересуется, то и стоит об это задуматься.
И все-таки мне удалось поработать с объектами, которые уменьшили как количество кода, так и его дублирование. Сперва напомню, что в файле bot.py
функции получения товаров категорий типа get_special_drinks()
, get_coffee()
, get_hot_drinks()
практически идентичны. И пусть они по объему маленькие, но что если бы каждая функция была по 100 строк? Именно это дело я и подкорректировал.
Отличие всех трех функций в том, что мы получаем получаем разные данные из словаря gh_menu типа gh_menu['сюда передаем категорию']
, и в разных состояниях, которые мы передаем в конце функций db_users.set_state(user_id, config.<сюда состояние>
.
Поэтому я перешел в файл config.py
и создал после всего имеющегося класс Goods.
Класс имеет конструктор def __init__()
, куда передаются следующие параметры:
self - это ссылка на экземпляр этого класса. В python он является обязательным, если вы работаете с классом. Если не в курсе про self, то погуглите, это не сложная, но важная тема.
bot - это объект нашего бота, который мы создаем в самом начале файла bot.py (
bot = telebot.TeleBot(config.TOKEN)
)message - это объект библиотеки pyTelegramBotAPI, он нужен для получения данных о пользователе, отправленным им текстом и еще много всего.
state - состояние пользователя, которое мы хотим ему присвоить
Также класс имеет функцию get_goods_list()
, в которую я и вынес весь функционал. Теперь я редактирую функции в bot.py