> For the complete documentation index, see [llms.txt](https://delaney.gitbook.io/create-telegram-bot/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://delaney.gitbook.io/create-telegram-bot/master.md).

# Мои первые шаги

## Как я учусь делать телеграм бота?

### Вступление

Приветствую всех, кто решил прочесть эту статью. В ней я постараюсь изложить все этапы создания бота, все материалы, что я прочитал, все ошибки, которые я совершил и осознал, в общем весь путь. Мое решение может быть плохим, так что я приветствую критику и предложения от более опытных и мудрых людей. Если хотите больше информации о моей деятельности, переходите в телеграм канал [**Debug\_Yourself**](https://t.me/debug_u).

### В чем идея бота?

Перед тем, как выбрать идею для бота, я понял, что лучше не пытаться сразу сделать "сложного" бота со сверхбогатым функционалом, потому что сложность на начальном этапе, как правило, всегда убивает энтузиазм и желание заниматься выбранным делом в принципе. Также я хотел сделать что-то более менее пригодное и актуальное, что позволило бы мне освоить весь базовый функционал, необходимый для реальных проектов. Скажем, можно было сделать криптобота, который бы позволял получать цену в $ интересующей крипты. И на самом деле я такой сделал - это слишком просто. Уверенности я в себе не почувствовал уж точно. Такой бот в чистом виде неактуален, хотя врать не буду, я поработал с API коинмаркеткапа, что уже неплохо.&#x20;

Я люблю выпить ягодный чай в кофейне, где кофе на вынос. Но свой заказ приходится ждать, неважно сколько, иногда время есть только на то, чтобы забежать, сразу взять свой чай и по делам. Но заказы онлайн они не принимают. И сразу идея - сделать бота для онлайн заказов. Это **автоматизация бизнеса**, что актуально. Это также тот **базовый функционал**, которые имеют большинство ботов, и более того, это возможно по готовности предлагать владельцам **купить такого бота.**&#x20;

{% hint style="success" %}
В общем решено - делаю бота для онлайн заказа в кофейне
{% endhint %}

###

### На чем я буду делать бота?

Разумеется, это никакой не конструктор. Я уже писал у себя на канале, почему лучше изучить прогу и написать бота, чем пользоваться конструкторами. Если коротко, то&#x20;

{% hint style="info" %}
Конструктор обычно решает шаблонные задачи, нетипичные проблемы всегда требует незаурядного ума.
{% endhint %}

Так как я немного знаю язык программирования (ЯП) **Python**, то и писать его буду на нем. Про достоинства этого языка можно написать отдельный пост (ну или прочитать в инете), поэтому сильно его нахваливать не буду. Лишь скажу, что в целом вы можете писать тг ботов на:

* PHP
* Java
* nodejs
* Python
* C#
* Go
* еще парочка других

Полный список можете глянуть по [ссылке](https://core.telegram.org/bots/samples). Там же вы увидите, что у популярных языков есть несколько библиотек. Python в частности имеет&#x20;

* [python-telegram-bot](https://github.com/python-telegram-bot/python-telegram-bot#learning-by-example)
* [Telepot](https://github.com/nickoala/telepot)
* [AIOGram](https://github.com/aiogram/aiogram)
* [twx.botapi](https://github.com/datamachine/twx.botapi)
* [**pyTelegramBotAPI**](https://github.com/eternnoir/pyTelegramBotAPI) (почему-то не указана в этом [списке](https://core.telegram.org/bots/samples#python))

Какая библиотека лучше? Разумеется, лично я сказать не могу, так как не имел опыта работы с ними. С другими тоже особо не говорил по этому поводу. Если судить по звездам на гитхабе, то **python-telegram-bot** обходит остальные. Потом идет **Telepot** и замыкает тройку **pyTelegramBotAPI**. Тем не менее я пользуюсь последней. Во многих обучающих примерах использовали именно ее, плюс она показалась довольно простой и привлекательной.&#x20;

Осталось упомянуть базу данных и используемую среду разработки. Я сразу понял, что без БД никуда, ну точнее можно без нее реализовать бота, но в реальных проектах вряд ли боты обходятся без БД, поэтому я решил ее использовать. MySQL/PostgreSQL я не знаю (за что даже стыдно), был опыт работы с SQLite, правда не в чистом виде, а когда изучал фреймворк Django (об этом как-нибудь потом), но там используется **ORM**. Если не знаете что такое ORM, могу порекомендовать быстро [прочесть тут](http://qaru.site/questions/32531/what-is-an-orm-and-where-can-i-learn-more-about-it), мне показалось довольно хорошим такое объяснение.... Возвращаясь к БД, я решил использовать **MongoDB**. У меня был небольшой опыт работы с ней, когда изучал *Node.js*, она мне сразу понравилась, и мне показалось, что она вполне годится для моего бота.&#x20;

В качестве среды разработки я использую [Visual Studio Code](https://code.visualstudio.com/). Опять же сред много, вы найдете свою только когда попробуете несколько.&#x20;

{% hint style="info" %}
Хотя я бы новичкам рекомендовал [**PyCharm**](https://www.jetbrains.com/pycharm/) - не ошибетесь.
{% endhint %}

### Первые шаги

Разумеется, изначально я посмотрел **документацию** выбранной библиотеки - это надо стараться делать первым делом, когда беретесь изучать что-то новое. Но и это правило имеет исключения. По моему мнению хорошая документацию должна содержать не только список имеющихся в ней "вещей", а непосредственно примеры их использования. Когда автор описывает способы реализации своих функций и дает к ним пояснения, он показывает свое видение. Тебе, как ученику, становится легче проследить его логику и понять надобность. Я читал документацию по Django и мне она понравилась. На тебя не валят сразу гору сухой информации, напротив, новичкам предлагают создать просто веб-приложение, где поясняется смысл почти каждой команды. Ты поэтапно собираешь пазлы, получая в конце всю картину. А уже после тебе дают более углубленные знания и эффективные приемы.&#x20;

Так вот касаемо библиотеки pyTelegramBotAPI - я не в восторге. Все что в ней описано - понятно, но не описываются нюансы, и ты не до конца понимаешь, как она себе поведет в той или иной ситуации. Конечно, есть вероятность, что виною всему моя неопытность. Тем не менее из всех остальных библиотек на python, эта показалась самой доступной.

**КОГДА УЖЕ ПРИСТУПИМ К СОЗДАНИЮ БОТА???**

Можно выдохнуть, уже! Условимся, что у вас установлена понравившаяся вам среда разработки, есть python. Причем [**python3**](https://www.python.org/). Версию можете ставить самую последнюю, на момент написания это python 3.7.&#x20;

Теперь надо создать бота. Для этого в тг существую "**крестный отец**" - с ником **@BotFather**. Жмете `/start`, потом `/newbot` , вводим имя нашего будущего бота, вводим его ник типа @something\_bot - в общем следуем инструкции. В случае успеха он пришлет нам token бота типа `Use this token to access the HTTP API: 431603025:AAGH3fmgfX6C_AuoiynFuMUgfsd9oPDzSo`

Именно с токеном мы и будем работать.

{% hint style="danger" %}
Никому не показывайте свой **TOKEN**, в противном случае вашим ботом смогут управлять третьи лица
{% endhint %}

У нас есть бот, можем кодить..... А вот тут еще одна загвоздка. У граждан гулагской федерации возникают трудности, ибо **телеграм запрещен**. При обращении к серверам телеграма, у меня начали возникать ошибки. Я понял, что сейчас VPN нужен как никогда. Не сказать, что я бедный, но платный брать не хотелось, по крайней мере на момент обучения. Среди бесплатных много говна. Но в итоге мне удалось найти хороший VPN, правда пришлось чуточку попотеть.

В одном из чатов добрые люди просто скинули ссылку <https://www.freeopenvpn.org/connect.php> и сказали следовать инструкции (она на сайте подробно описана, также я для своих подписчиков на всякий случай описал свою [инструкцию](https://tgraph.io/Nastraivaem-sebe-besplatnyj-VPN-za-5-minut-11-04)).&#x20;

1. Скачать прогу в зависимости от вашей ОС
2. Далее выбираем [понравившийся сервер](https://www.freeopenvpn.org/)
3. Качаем файл с настройками.&#x20;
4. Вводим логин freeopenvpn и пароль, которые меняется раза два в сутки вроде

Скорость сильно не падает, зато теперь я смог запускать бота, и он работал.&#x20;

Кстати, я не уточнил, сам я использую **macOS**, винду не очень люблю и вам не советую. **Ставьте linux**. Либо второй системой рядом с виндой, либо на виртуальной машине. Расписывать про выбор дистрибутива и его установку смысла не вижу, по крайней мере тут. Если будет желание и необходимость, отдельно сделаю. Сам я с линуксом мало работал, плюс начинал знакомство сразу с довольно хардкорного фреймфорка - [Arch Linux](https://www.archlinux.org/).&#x20;

### Начинаем кодить

Когда я изучал веб-программирование, в одном уроке хороший человек сказал, что в проектах надо использовать **виртуальное окружение**. Потому что у каждой библиотеки есть свои версии, иногда новые версии несовместимы со старыми, отсюда могут возникать ошибки. Банальный пример с самим питоном: у вас несколько проектов, один из которых на втором питоне, другой на третьем. А эти версии без **обратной совместимости**! Помимо этого вы очень трудолюбивый и имеет еще три проекта на Django, где у одного версия 1.10, у второго 2.0 и третьего 1.8.&#x20;

{% hint style="info" %}
Именно в таком случае при создании каждого проекта создается виртуальное окружение, изолируя проект от вне. Это позволяет вам использовать в каждом проекте именно необходимые вам версии библиотек.&#x20;
{% endhint %}

Таким образом вы устанавливаете все требуемые библиотеки, сохраняете их версии в специальном файле **requirements.txt**, и когда кто-то захочет воспользоваться вашим творением, ему достаточно будет установить все из этого файла. Подробную инфу можете [глянуть здесь](https://virtualenv.pypa.io/en/latest/), либо [посмотреть это видео](https://www.youtube.com/watch?v=v3PIblL_Kq8).

Напомню, я пишу на mocOS, но команды также будут актуальны (не все) и для Linux. Оконщикам придется перебираться на другую систему (но вам будет полезно, **обещаю**).&#x20;

Виртуальная среда у меня уже была, но вообще она устанавливается так

```bash
pip3 install virtualenv
```

**pip3** - это менеджер пакетов для python, при чем для python 3 будет pip3, для python 2 просто pip. **virtualenv** - это название пакета виртуального окружения

Итак, я создал каталог на рабочем столе **gh\_coffee\_bot.** Открыв его в терминале, я ввожу команду&#x20;

```bash
virtualenv venv
```

&#x20;**venv** - это мое название для созданного виртуального окружения.&#x20;

Внутри **gh\_coffee\_bot** появился каталог **venv**.

Также я создал внутри каталога **gh\_coffee\_bot** еще один с названием **app**. В нем я храню файлы с ботом.

Но установить вирт.окружение мало, надо его активировать. В каталоге **gh\_coffee\_bot** в терминале достаточно прописать

```bash
source venv/bin/activate
```

и оно станет активным, понять это можно по добавленному (venv) слева строки терминала.

Также можно посмотреть, что уже установлено командой

```bash
pip3 freeze
```

Вирт.окружение у нас пустое, поэтому там ничего не будет.

Далее я установил саму библиотеку **pyTelegramBotAPI**

```bash
pip3 install pyTelegramBotAPI
```

&#x20;Создал в каталоге **app** файл **bot.py**, это будет главный исполняемый файл. В нем сначала импортировал все требуемые модули и попробовал написать просто бота, который бы отвечал нам на любое наше сообщение, дабы удостовериться, что все будет работать дальше.

{% code title="bot.py" %}

```python
import telebot

bot = telebot.TeleBot(тут мой токер)

@bot.message_handler(content_type=['text'])
def echo_message(message):
    bot.send_message(message.from_user.id, 'Привет, друже')
```

{% endcode %}

Не знаю, стоит ли пояснять все детально. Здесь просто идет импорт библиотеки на 1 строчке, на 3 я создал экземпляр класса TeleBot(), куда передал свой токен в виде строки, а с 5-7 строки начинают творить магию, о ней будет ниже. Могу посоветовать прочитать [официальную документацию](https://github.com/eternnoir/pyTelegramBotAPI) и повторить все примеры.&#x20;

Код выше работал, а значит можно уже задуматься над какой-то структурой, ибо мешать все в один файл - это гиблое дело.&#x20;

Во-первых, пришла в голову мысль создать файл **config.py**, куда я отдельно записал свой **token**, а также пару функций, которые напрямую к боту не относятся (но о функциях позже).

{% code title="config.py" %}

```python
from telebot import types

TOKEN = 'ваш токен'
```

{% endcode %}

Во-вторых, у меня будет БД, значит надо создать отдельный файл для работы с ней. Пусть пока что мой бот будет просто сохранять всех новых пользователей, а именно их **first\_name** и **last\_name** (возможно, это и бессмысленно, потому что юзеры могут их спокойно менять, но пока что пусть будут). Также обязательно буду сохранять их **user\_id**, он у каждого уникальный в тг, значит по этому параметру мы потом будет к этим юзерам обращаться и получать какие-либо требуемые данные. Не помешает сохранять время "регистрации" пользователям, то есть когда он добавляется в БД. И также я добавлю пользователю еще один параметр **state**, об этом я опишу чуть позже.

Для работы с **MongoDB** я разумеется установил саму mongo (команда актуальна для macOS)

```bash
brew install mongodb
```

Установил пакет **pymongo**.&#x20;

```bash
pip3 install pymongo
```

Также я скачал программу [Robo 3T](https://robomongo.org/), чтобы можно было визуально работать с базой данных. Чтобы запустить установленную БД, ввел&#x20;

```bash
mongod
```

Создал файл **db\_users.py** (это все в каталоге app), где подключился к БД и написал три функции

{% code title="db\_users.py" %}

```python
import pymongo
from pymongo import MongoClient
from datetime import datetime
import config

client = MongoClient('localhost', 27017)

db = client['gh_coffee_db']


def check_and_add_user(message):
    if db.users.find_one({'user_id': message.from_user.id}) == None:
        new_user = {
            'first_name': message.from_user.first_name,
            'last_name': message.from_user.last_name,
            'user_id': message.from_user.id,
            'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
            'state': 'старт'
        }
        db.users.insert_one(new_user)
    return

def get_current_state(user_id):
    user = db.users.find_one({'user_id':user_id})
    return user['state']


def set_state(user_id, state_value):
    db.users.update_one({'user_id': user_id}, {"$set": {'state': state_value}})


```

{% endcode %}

Логика довольно простая: `check_and_add_user(message)` проверяет, есть ли пользователь в нашей БД по его id, который получаем при помощи **message.from\_user.id**, `get_current_state(user_id)` позволяет нам получить текущее состояние пользователя (значение параметра **state**), ну и при помощи `set_state()` мы устанавливаем пользователю новое состояние. На самом деле с "состояниями" я немного поторопился. Зачем они вообще нужны и в чем их логика? Пока что я скажу так - **забудьте на время про них вообще**, когда я дойду до этапа, на котором я до них додумался, я про них расскажу.

Теперь мы может перейти в наш файл **bot.py**, и начинать писать что-то серьезное. Хотя не совсем. Я вроде понимаю, что должен делать бот: сначала выбирать общий раздел напитков, потом выбирать конкретный напиток, после выбор размера. После всего в идеале товар добавляется в корзину и так можно выбрать несколько, а потом уже оплатить. Но сколько всего разделов, сколько напитков/сендвичей и прочей продукции. В каком виде вообще хранить эту информацию?&#x20;

Сначала я задумался о том, что надо все хранить в БД. Но такая задумка меня напугала своей сложностью реализации, поэтому я решил это дело оставить на вторую версию своего бота. Что я сделал? Нашел кофейню, где на сайте есть меню и взял его часть. Пока что полностью оно мне не нужно, мне важно простроить логику, а добавить оставшуюся продукцию всегда можно.&#x20;

Таким образом я создал еще один файл в каталоге **app** под названием **gh\_menu.py**. В нем я создал словарь **gh\_menu**, куда добавил часть меню:

{% code title="gh\_menu.py" %}

```python
gh_menu = {
    'Особые напитки': {
        'Латте Лаванда Шалфей': {
            '300 мл - 139 руб': '139',
            '400 мл - 169 руб': '169'
        },
        'Раф Лимонный Пай': {
            '300 мл - 159 руб': '159',
            '400 мл - 179 руб': '179'
        },
        'Раф Шоколадный трюфель': {
            '300 мл - 159 руб': '159',
            '400 мл - 179 руб': '179'
        },
        'Кедровый раф': {
            '300 мл - 169 руб': '169',
            '400 мл - 219 руб': '219'
        },
        'Капучино Соленая карамель': {
            '300 мл - 139 руб': '139',
            '400 мл - 169 руб': '169'
        },
        'Зеленый капучино': {
            '300 мл - 169 руб': '169',
            '400 мл - 239 руб': '239'
        }
    },
    'Кофе': {
        'Капучино': {
            '300 мл - 119 руб': '119',
            '400 мл - 149 руб': '149'
        },
        'Латте Макиато': {
            '300 мл - 119 руб': '119',
            '400 мл - 139 руб': '139'
        },
        'Раф': {
            '300 мл - 149 руб': '149',
            '400 мл - 169 руб': '169'
        },
        'Айс Крим Латте': {
            '300 мл - 129 руб': '129',
            '400 мл - 149 руб': '149'
        },
        'Мокко': {
            '400 мл - 179 руб': '149'
        }
    },
    'Горячие напитки': {
        'Какао': {
            '300 мл - 119 руб': '119',
            '400 мл - 149 руб': '149'
        },
        'Чай': {
            'Английский завтрак': {
                '300 мл - 99 руб': '99',
                '400 мл - 109 руб': '109'
            },
            'Эрл грей': {
                '300 мл - 99 руб': '99',
                '400 мл - 109 руб': '109'
            },
            'Моргентау': {
                '300 мл - 99 руб': '99',
                '400 мл - 109 руб': '109'
            },
            'Жасмин голд': {
                '300 мл - 99 руб': '99',
                '400 мл - 109 руб': '109'
            },
            'Апельсин ройбуш': {
                '300 мл - 99 руб': '99',
                '400 мл - 109 руб': '109'
            }
        },
        'Горячее молоко': {
            '300 мл - 99 руб': '99',
            '400 мл - 119 руб': '119'
        }
    }
}
```

{% endcode %}

Честно говоря я не знаю пока, насколько удачно я придумал это. Время покажет...

Ну теперь то я могу перейти в файл **bot.py** и начать кодить.&#x20;

Импортировал все необходимые модули

{% code title="bot.py" %}

```python
import telebot
from telebot import types
from gh_menu import gh_menu
import config
import db_users
```

{% endcode %}

Создание объекта бота теперь немного изменилось

{% code title="bot.py" %}

```python
bot = telebot.TeleBot(config.TOKEN)
```

{% endcode %}

Командой `config.TOKEN` мы обращаемся к нашему модулю **config** к созданной переменной **TOKEN**.

Сначала нашего пользователя надо поприветствовать и коротко объяснить суть происходящего. Для этого создал функцию `send_welcome(message)` , которая будет вызываться тогда, когда пользователь первый раз зайдет в бота, ну либо введет команду **/start**

{% code title="bot.py" %}

```python
text_messages = {
    'start':
        u'Приветствую тебя, {name}!\n'
        u'Я помогу тебе сделать онлайн заказ (это быстро и без очереди).\n\n'
        u'1. Выбери интересующий напиток/сэндвич/десерт (Ты можешь выбрать несколько)\n'
        u'2. Выбери время, когда захочешь забрать заказ\n'
        u'3. Оплати заказ (это безопасно)\n'
        u'4. Обязательно забери заказ вовремя',
    
    'help':
        u'Пока что я не знаю, чем тебе помочь, поэтому просто выпей кофе!'
}

@bot.message_handler(commands=['start'])
def send_welcome(message):
    db_users.check_and_add_user(message)

    markup = types.ReplyKeyboardMarkup(resize_keyboard=True)
    markup.row('Сделать заказ')
    bot.send_message(message.from_user.id, text_messages['start'].format(name=message.from_user.first_name), reply_markup=markup)
```

{% endcode %}

В созданной функции мы как раз таки первым делом проверяем, есть данный пользователь или нет, и если нет, то добавляем его (это 16 строчка).  18-19 строчки отвечают за создание кастомной клавиатуры, в моем случае будет лишь одна кнопка "*сделать заказ*". На 20 строчке мы вызываем объект **bot**, у него есть метод `send_message()` куда мы первым должны передать id нашего пользователя (некоторые делают это так `message.chat.id`, **но я считаю правильнее получать именно id пользователя, а не чата**), также надо передать текст сообщения, но так как мое сообщение длинное, для эстетического баланса я создал словарь `text_messages`,  у которого есть два ключа **start** и **help.** Так как мы отвечаем на пользовательскую команду `/start`, то и вызываем значение по этому ключу. Методом `format` я лишь передаю в этот словарь имя нашего пользователя, которое содержится в объекте **message**. Третий параметр reply\_markup опциональный, но так как я создал клавиатуру, то передаю ее `reply_markup=markup`.&#x20;

Вообще в хэндлере `@bot.message_handler()` я могу написать разные условия: например, если мне надо, чтобы бот реагировал на команды через `/`, типа `/start` или `/help`, то пропишу `@bot.message_handler(commands=['start', 'help'])` , или могу разделить и написать для каждой команды отдельно. Если я хочу, чтобы бот реагировал на текстовое сообщение/эмодзи (а эмодзи, это тоже текст, если я не ошибаюсь), то пропишу `@bot.message_handler(content_types=['text'])` , и по аналогии вместо text можно вставлять аудио, документы и прочие объекты тг. Функционал моего бота пока ограничится текстом.

Теперь мне надо мою идею из головы как-то конвертировать в алгоритм хотя бы на бумаге, а в итоге в код. Изначально пользователь видит приветствие бота, читает инструкцию и жмет на кнопку "Сделать заказ". Ему должно показаться меню с выбором категорий, типа

* Особые напитки
* Кофе
* Горячие напитки

При выборе категории пользователю надо показать меню с перечнем конкретных товаров. К примеру, если это будут "Особые напитки", то надо отобразить:

* Латте Лаванда Шалфей
* Раф Лимонный Пай
* Раф Шоколадный трюфель
* Кедровый раф
* Капучино Соленая карамель
* Зеленый капучино

И так уже для каждой категории. При выборе конкретного товара надо отобразить доступные вариации, к примеру, если это напиток, то надо указать размер стаканчика, если это какой-нибудь сэндвич, то надо указать вариации, типа "с лососем" или "с курицей".&#x20;

Честно говоря, на время написания этого куска статьи я уже попробовал одну реализацию бота, которая **потерпела крах**. Но я считаю нужным сначала рассказать о ней, чтобы вы смогли увидеть мою ~~логику~~ (или ее отсутствие), посмотреть на ошибки, как я постарался их исправить, а после пока то, что имею на данный момент.

Итак, я понял, что в итоге бот должен отрисовывать пользователю меню, получать какой-то текст, понимать, что требуется пользователь и отрисовывать следующее меню. И так до оплаты. Теперь меньше абстракции:&#x20;

1. Этап - пользователь зашел в бота либо нажал `/start`
2. Этап - выбирает категорию
3. Этап - выбирает конкретный продукт выбранной категории
4. Этап - Выбирает размер напитка/вид сэндвича и тп
5. Этап - по идее выбор добавляется в корзину (надо потом в БД создать отдельную переменную и сохранять туда), после чего можно предложить еще что-нибудь, циклично вернув на 2 этап, либо оплата
6. Оплата

В голову сразу идея приходит - для каждого этапа напишу свою функцию. В итоге файл `bot.py` выглядел примерно так:

{% code title="bot.py" %}

```python
### выше предыдущий код

@bot.message_handler(content_type=['text'])
def choose_categories(message):
    cat_markup = types.ReplyKeyboardMarkup(resize_keyboard=True)
    cat_markup.row('Особые напитки', 'Кофе')
    cat_markup.row('Горячие напитки', 'еще что-то')
    msg = bot.send_message(message.from_user.id, 'Что вас интересует?', reply_markup=cat_markup)
    bot.register_next_step_handler(msg, choose_drink)

    
@bot.message_handler(content_type=['text'])
def choose_drink(message):
    drink_markup = types.ReplyKeyboardMarkup(resize_keyboard=True)
    
    if message.text == 'Особые напитки':
        drink_markup.row('Латте Лаванда Шалфей', 'Раф Лимонный Пай')
        drink_markup.row('Раф Шоколадный трюфель','Кедровый раф')
        drink_markup.row('Капучино Соленая карамель', 'Зеленый капучино')
    elif message.text == 'Кофе':
        drink_markup.row('Капучино', 'Латте Макиато', 'Раф')
        drink_markup.row('Айс Крим Латте', 'Мокко')
    elif message.text == 'Горячие напитки':
        drink_markup.row('Какао', 'Чай', 'Горячее молоко')
    drink_markup.row('Назад')

    msg = bot.send_message(message.from_user.id, 'Выберите напиток', reply_markup=drink_markup)
    bot.register_next_step_handler(msg, choose_size)


@bot.message_handler(content_type=['text'])
def choose_size(message):
    size_markup = types.ReplyKeyboardMarkup(resize_keyboard=True)
    if message.text == 'Назад':
        choose_categories(message)
        return 
    else:
        if message.text == 'Латте Лаванда Шалфей':
            size_markup.row('300 мл - 139 руб', '400 мл - 169 руб')
        elif message.text == 'Раф Лимонный Пай':
            size_markup.row('300 мл - 159 руб', '400 мл - 179 руб')
        elif message.text == 'Раф Шоколадный трюфель':
            size_markup.row('300 мл - 159 руб', '400 мл - 179 руб')
        elif message.text == 'Кедровый раф':
            size_markup.row('300 мл - 169 руб', '400 мл - 219 руб')
        elif message.text == 'Капучино Соленая карамель':
            size_markup.row('300 мл - 139 руб', '400 мл - 169 руб')
        elif message.text == 'Зеленый капучино'
            # здесь мне показалось, что я делаю что-то жесткое, поэтому
            # перестал создавать оставшиеся elifы
        size_markup.row('Назад')
        msg = bot.send_message(message.from_user.id, 'Выбери размер', reply_markup=size_markup)


def get_paid(message):
    # До оплаты еще далеко, поэтому эту функцию описывать сейчс смысла нет
    pass



bot.polling()
```

{% endcode %}

Что можно видеть? Все, как и описывал выше. Для **2 этапа** написал функцию `def choose_categories(message)` , для **3 этапа** `def choose_drink(message)`, и затронул немного **4 этап** `def choose_size(message)`. Остановился, чтобы проанализировать, что я имею. Еще забыл упомянуть, что наличие хэндлеров подразумевает, что вызовется та функция, которая первая подходит по условию. Конечно, у меня сейчас везде один и тот же хэндлер, что мне уже не нравится, но это стоит учитывать, потому что изначально я этого не сделал.&#x20;

Для того, чтобы из одной функции вызвать другую, можно использовать `register_next_step_handler()` , куда надо передать объект **message** и, собственно, **название самой функции**. В принципе, в таком случае вообще можно оставить хэндлер только у функции **choose\_categories()** , а остальные убрать. Но такая концепция подошла бы, если путь в боте был прямолинейный. А ведь пользователь может ошибиться и захотеть вернуться назад, и тут `register_next_step_handler()` терпит крах. Что я имею ввиду: допустим юзер уже зашел в категорию, выбирает кофе, но передумал и хочет вернуться опять в "категории". На словах все просто, а как дела с кодом обстоят? Тут небольшое безумие. В боте кнопки кнопками как таковыми не являются, то есть **кнопки - это не отдельный объект, это просто удобный для пользователя интерфейс**. Мы, как программист, пишем программу, которая умеет работать только с определенным набором команд (вводимым пользователем текстом). И мы эти команды ждем от пользователям, поэтому и создаем кнопки, чтобы пользователь мог, нажав на них, отправить нужный нам текст. Это не кнопки, как в том же веб-программировании.&#x20;

Это я все пишу для того, чтобы дать понять, что нажав на кнопку, юзер точно также отправляет текст, как если бы он просто его ввел. Ну это наверное и так очевидно. И когда наш пользователь, допустим, находится в функции выбора напитков `choose_drink()` и жмет на кнопку **назад**, чтобы перейти снова к выбору категорий (для нас это функция `choose_categories()` ) то по факту он все равно сначала попадет в функцию `choose_size()`, потому что **обработать отправленный текст**, насколько я понял, **можно только в следующей функции.**

И, как можно видеть, я как раз добавил в самом начале `choose_size()` условие if

{% code title="bot.py" %}

```python
@bot.message_handler(content_type=['text'])
def choose_size(message):
    size_markup = types.ReplyKeyboardMarkup(resize_keyboard=True)
    if message.text == 'Назад':
        choose_categories(message)
        return 
    else:
    # продолжение
```

{% endcode %}

&#x20;Если полученный текст `message.text` равен "назад", то просто вызываю функцию получения категорий. Сначала я пытался это провернуть путем вызова `register_next_step_handler()` , но возникал баг: чтобы перейти назад, надо было боту отправить два раза что-нибудь (то есть сначала ты тыкаешь "назад", бот замирает, потом еще раз что-нибудь пишешь, и он отправлял в функцию выбора категорий). Так что и не пытайтесь делать также (разве только в качестве опыта).

![Наглядный пример бага ](/files/-LQhdpLjC8TFeV20Wi6s)

### Работа над ошибками

Первое, что можно заметить, это повтор кода. Прямое нарушение принципа [DRY](https://ru.wikipedia.org/wiki/Don%E2%80%99t_repeat_yourself). В каждой функции я заново прописываю создание объекта кастомной клавиатуры, создаю отдельные кнопки путем `markup.row('что-то')` . Разумеется, на ум приходит идея написать функцию, которая бы сама создавала клавиатуру, а мы бы просто туда передавали название кнопок.&#x20;

Для этого я создал в файле **config.py** функцию `create_menu()`

{% code title="config.py" %}

```python
def create_menu(mass, back=True):
    """
    This function allows to creat menu of buttons.
    mass - the list of string
    back - back button, if true, add a button back. Default back=True
    """
    markup = types.ReplyKeyboardMarkup(resize_keyboard=True)

    if len(mass) == 1:
        markup.row(mass[0])
    else:
        while len(mass) > 0:
            try:
                cut = mass[:2]
                markup.row(cut[0], cut[1])
                del mass[:2]
                
                if len(mass) == 1:
                    markup.row(mass[0])
                    break
            except:
                print('WTF')
    

    if back == True:
        markup.row('Назад')
    
    return markup
```

{% endcode %}

Итак. Функция требует два параметра: **mass** - это список наших кнопок по типу `['Горячие напитки', 'Кофе', 'Особые напитки']` , **back** - это булевская переменная, если она будет  **True**, то в наше меню будет добавляться кнопка "Назад". Она нам может быть не всегда нужна, поэтому это вполне логичное решение.

Сразу оговорюсь, что на мой взгляд оптимальным будет сделать клаву в **2 столбца**. В 1 слишком широко, в 3 надписи не влезают. А мы все таки хотим, чтобы интерфейс был приятным.

На 7 строке создал объект клавиатуры. Далее начинаю проверять, если массив содержит лишь один элемент, то просто его добавляем и на этом все. Если же >1, то мы переходим в цикл `While`, где просто **делаем срез** полученного списка на 14 строке (первые два элемента), добавляем элементы среза в клавиатуру и удаляем его(это строки 15-16). И так циклично, пока длина массива элементов не станет равной единице. В этом случае сработает условие на 18 строке, добавится последний элемент и произойдет выход из цикла. И на 25 строке произойдет проверка, если `back=True` , то в клаву еще добавиться кнопка "Назад".&#x20;

Конечно, можно еще немного попотеть и сделать более гибкой функцию в плане того, чтобы выбирать количество столбцов в клаве (хотя их можно быть не больше 3х), но я посчитал для себя это лишним гемором.

Теперь хочу приоткрыть тайну загадочного поля **"state"** у наших юзеров. Вот так сейчас выглядит юзер (это я)  в БД

![Отображение юзеров через RoboMongo](/files/-LRGgvl4O2s5jNfdssMi)

И хотя я могу называть юзеров объектами, с точки зрения самой БД наверное правильно называть их документами. MongoDB является документоориентированной БД. И при работе с ней вы можете каждый свой "объект" сохранять в виде документа. Документы хранятся в коллекциях. В моей случае документы юзеров хранятся в коллекции **users**. Не буду сильно углубляться в структуру Монги, прочтите сами и поймете, о чем я говорю.

Так что же за **state**, показанный на скрине "**Выбор категории**"? Для того, чтобы в процессе работы бота вызывать требуемые нам функции, надо иметь какой-то параметр, в зависимости от которого они и буду вызываться. То есть я создал у юзера поле **state**, и теперь могу присвоить значение "**Выбор категории**" и написать функцию, которая будет вызываться только тогда, когда значение `state="Выбор категории"`.

{% hint style="info" %}
Другими словами, функции в боте будут вызываться в зависимости от состояния. Ну и логично, что у каждого состояния должна быть своя функция
{% endhint %}

Теперь хочу снова обратить внимание на файл `db_users.py`

{% code title="db\_users.py" %}

```python
import pymongo
from pymongo import MongoClient
from datetime import datetime
import config

client = MongoClient('localhost', 27017)

db = client['gh_coffee_db']


def check_and_add_user(message):
    if db.users.find_one({'user_id': message.from_user.id}) == None:
        new_user = {
            'first_name': message.from_user.first_name,
            'last_name': message.from_user.last_name,
            'user_id': message.from_user.id,
            'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
            'state': 'старт'
        }
        db.users.insert_one(new_user)
    return

def get_current_state(user_id):
    user = db.users.find_one({'user_id':user_id})
    return user['state']


def set_state(user_id, state_value):
    db.users.update_one({'user_id': user_id}, {"$set": {'state': state_value}})

```

{% endcode %}

Тут я и работаю с состояниями. Я даже еще раз повторю логику функционала. А Логика довольно простая: `check_and_add_user(message)` проверяет, есть ли пользователь в нашей БД по его id, который получаем при помощи **message.from\_user.id.** Если пользователя нет, то добавляем **new\_user**. `get_current_state(user_id)` позволяет нам получить текущее состояние пользователя (значение параметра **state**), ну и при помощи `set_state()` мы устанавливаем пользователю новое состояние.

Сами состояния я пока храню в виде констант в файле `config.py` по типу:

{% code title="config.py" %}

```python
from telebot import types

TOKEN = 'ваш токен'

# КОНСТАНТЫ СОСТОЯНИЙ
S_GET_CAT = 'Список категорий'
S_CHOOSE_CAT = 'Выбор категории'

# КОНСТАНТЫ КАТЕГОРИЙ
S_SPECIAL_DRINKS = 'Особые напитки'
S_COFFEE = 'Кофе'
S_HOT_DRINKS = 'Горячие напитки'

# КОНСТАНТЫ ТОВАРОВ
S_CHOOSE_GOOD = 'Выбор товара'

S_LATTE_LAVANDA_SHALFEI = 'Латте Лаванда Шалфей'
S_RAF_LEMON_PIE = 'Раф Лимонный Пай'
S_RAF_CHOCOLATE_TRUFEL = 'Раф Шоколадный трюфель'
S_KEDROVI_RAF = 'Кедровый раф'
S_KAPUCHINO_SALT_KARAMEL = 'Капучино Соленая карамель'
S_GREEN_KAPUCHINO = 'Зеленый капучино'

S_KAPUCHINO = 'Капучино'
S_LATTE_MAKIATO = 'Латте Макиато'
S_RAF = 'Раф'
S_ICE_CREAM_LATTE= 'Айс Крим Латте'
S_MOKKO = 'Мокко'

S_KAKAO = 'Какао'
S_TEA = 'Чай'
S_HOT_MILK = 'Горячее молоко'
```

{% endcode %}

Это еще не весь список. Дело в том, что меня пугает такое большое количество состояний. И знаете почему? Потому что я писал выше, что на каждое состояние требуется своя функция. И получается их довольно много, одни скорее всего однотипные. И я пока не придумал, как уменьшить код.

Ну хорошо, теперь стоит показать вам как преобразился файл `bot.py`

{% code title="bot.py" %}

```python
import telebot
from telebot import types
from gh_menu import gh_menu
import config
import db_users
from config import create_menu

bot = telebot.TeleBot(config.TOKEN)

text_messages = {
    'start':
        u'Приветствую тебя, {name}!\n'
        u'Я помогу тебе сделать онлайн заказ (это быстро и без очереди).\n\n'
        u'1. Выбери интересующий напиток/сэндвич/десерт (Ты можешь выбрать несколько)\n'
        u'2. Выбери время, когда захочешь забрать заказ\n'
        u'3. Оплати заказ (это безопасно)\n'
        u'4. Обязательно забери заказ вовремя',
    
    'help':
        u'Пока что я не знаю, чем тебе помочь, поэтому просто выпей кофе!'
}

@bot.message_handler(commands=['start'])
def send_welcome(message):
    db_users.check_and_add_user(message)

    markup = types.ReplyKeyboardMarkup(resize_keyboard=True)
    markup.row('Сделать заказ')
    bot.send_message(message.from_user.id, text_messages['start'].format(name=message.from_user.first_name), reply_markup=markup)
    db_users.set_state(message.from_user.id, config.S_GET_CAT)


@bot.message_handler(func=lambda message: db_users.get_current_state(message.from_user.id) == config.S_GET_CAT)
def get_categories(message):
    """
    This function allows to get a list of categories
    """
    user_id = message.from_user.id
    mass = list(gh_menu.keys())
    markup = create_menu(mass, back=False)
    bot.send_message(user_id, 'Что вас интересует?', reply_markup=markup)
    db_users.set_state(user_id, config.S_CHOOSE_CAT)


# !!!  ЭТО ПРОМЕЖУТОЧНАЯ ФУНКЦИЯ  !!!
@bot.message_handler(func=lambda message: db_users.get_current_state(message.from_user.id) == config.S_CHOOSE_CAT)
def choose_categories(message):
    """
    This function allows to choose a category
    """
    user_id = message.from_user.id

    if message.text == 'Особые напитки':
        db_users.set_state(user_id, config.S_SPECIAL_DRINKS)
        get_special_drinks(message)
    elif message.text == 'Кофе':
        db_users.set_state(user_id, config.S_COFFEE)
        get_coffee(message)
    elif message.text == 'Горячие напитки':
        db_users.set_state(user_id, config.S_HOT_DRINKS)
        get_hot_drinks(message)


@bot.message_handler(func=lambda message: db_users.get_current_state(message.from_user.id) == config.S_SPECIAL_DRINKS)
def get_special_drinks(message):
    """
    This function allows to get a list of special drinks
    """
    user_id = message.from_user.id
    mass = list(gh_menu['Особые напитки'])
    markup = create_menu(mass)
    bot.send_message(user_id, 'Выберите напиток', reply_markup=markup)
    db_users.set_state(user_id, config.S_CHOOSE_GOOD)


@bot.message_handler(func=lambda message: db_users.get_current_state(message.from_user.id) == config.S_COFFEE)
def get_coffee(message):
    """
    This function allows to get a list of coffee
    """
    user_id = message.from_user.id
    mass = list(gh_menu['Кофе'])
    markup = create_menu(mass)
    bot.send_message(user_id, 'Выберите напиток', reply_markup=markup)
    db_users.set_state(user_id, config.S_CHOOSE_GOOD)


@bot.message_handler(func=lambda message: db_users.get_current_state(message.from_user.id) == config.S_HOT_DRINKS)
def get_hot_drinks(message):
    """
    This function allows to get a list of hot drinks
    """
    user_id = message.from_user.id
    mass = list(gh_menu['Горячие напитки'])
    markup = create_menu(mass)
    bot.send_message(user_id, 'Выберите напиток', reply_markup=markup)
    db_users.set_state(user_id, config.S_CHOOSE_GOOD)


# ЭТО ПРОМЕЖУТОЧНАЯ ФУНКЦИЯ
@bot.message_handler(func=lambda message: db_users.get_current_state(message.from_user.id) == config.S_CHOOSE_GOOD)
def choose_good(message):
    """
    This function allows to choose a goods
    """
    user_id = message.from_user.id

    if message.text == "Назад":
        db_users.set_state(user_id, config.S_GET_CAT)
        get_categories(message)
    if message.text == 'Латте Лаванда Шалфей':
        db_users.set_state(user_id, config.S_LATTE_LAVANDA_SHALFEI)
    elif message.text == 'Раф Лимонный Пай':
        db_users.set_state(user_id, config.S_RAF_LEMON_PIE)
    elif message.text == 'Раф Шоколадный трюфель':
        db_users.set_state(user_id, config.S_RAF_CHOCOLATE_TRUFEL)
    elif message.text == 'Кедровый раф':
        db_users.set_state(user_id, config.S_KEDROVI_RAF)
    elif message.text == 'Капучино Соленая карамель':
        db_users.set_state(user_id, config.S_KAPUCHINO_SALT_KARAMEL)
    elif message.text == 'Зеленый капучино':
        db_users.set_state(user_id, config.S_GREEN_KAPUCHINO)
    elif message.text == 'Капучино':
        db_users.set_state(user_id, config.S_KAPUCHINO)
    elif message.text == 'Латте Макиато':
        db_users.set_state(user_id, config.S_LATTE_MAKIATO)
    elif message.text == 'Раф':
        db_users.set_state(user_id, config.S_RAF)
    elif message.text == 'Айс Крим Латте':
        db_users.set_state(user_id, config.S_ICE_CREAM_LATTE)
    elif message.text == 'Мокко':
        db_users.set_state(user_id, config.S_MOKKO)
    elif message.text == 'Какао':
        db_users.set_state(user_id, config.S_KAKAO)
    elif message.text == 'Чай':
        db_users.set_state(user_id, config.S_TEA)
    elif message.text == 'Горячее молоко':
        db_users.set_state(user_id, config.S_HOT_MILK)


@bot.message_handler(func=lambda message: db_users.get_current_state(message.from_user.id) == config.S_LATTE_LAVANDA_SHALFEI)
def get_latte_lavanda_shalfei(message):
    user_id = message.from_user.id
    mass = list(gh_menu['Особые напитки']['Латте Лаванда Шалфей'])
    markup = create_menu(mass)
    bot.send_message(user_id, 'Выберите размер', reply_markup=markup)
    db_users.set_state(user_id, )

# Еще писать и писать...

bot.polling()
```

{% endcode %}

Поясню некоторые моменты, начиная сверху.

В функции `send_welcome()` добавилась строка 30, где я из модуля `db_users` вызываю функцию `set_state` чтобы установить новое состояние, следуя философии, описанной выше. Вызов этой функции вы можете наблюдать в конце каждой функции файла `bot.py` либо условия `if`, ну это логично. Как только функция отработала, нам надо установить новое состояние пользователя и исходя из него будет вызвана следующая функция. Но как именно? А вы обратили внимание, что теперь у каждой функции поменялся хэндлер (к примеру строка 33). Вот как он сейчас выглядит&#x20;

```python
@bot.message_handler(func=lambda message: db_users.get_current_state(message.from_user.id) == config.S_GET_CAT)
```

Теперь в хэндреле происходит получение текущего состояния пользователя из БД путем вызова функции `get_current_state()` в модуле `db_users`. И если полученное состояние равно необходимому нам, в данном случае состояние вызова категорий, полученное из модуля (файла) `config.py`.&#x20;

По такой логике я решил сделать всего бота. Преимущество в том, что теперь легко управлять тем, когда и что будет вызываться. Но количество кода вырастает в несколько раз. Я думаю все дело в том, что и бот и библиотека в целом больше функциональные. Нету ООПа. Невозможно работать с абстрактными объектами. Возможно, мне придет в голову решение, как организовать лучше. Но пока будет так.

Также я бы обратил ваше внимание на строку 40, где я создаю меню при помощи написанной мной же функции `create_menu()` , и туда передаю список, так сказать, слов (строк), которые в меню должны отображаться. И я не создаю новый список с этими строками, а делаю волшебство на строке 39. Ведь у нас есть файл `gh_menu.py`, в котором создан одноименный словарь **gh\_menu**. Я просто получил все его ключи. Опять же преимущество в том, что при правках в меню, мне стоит поменять лишь словарь **gh\_menu**, при этом не беспокоясь о клавиатуре.

По ходу написания кода я понял одну вещь: вот мы в функции `get_categories()`показали пользователю меню категорий. Каждая категория подразумевают собой еще функцию с отрисовкой другого меню для пользователя. Но как из первой функции вызвать вторую, причем надо вызвать нужную, то есть при выборе "особых напитков" к примеру надо вызвать `get_special_drinks()` и тп. Для этого пришлось написать еще одну функцию `choose_categories()` на строке 47, прокоментить как промежуточную функцию,  ведь в ней по факту просто происходит условие выбора. Такой же промежуточной функцией является и `choose_good()`(то есть выбор товара) на 102 строке, где точно также происходит выбор товара.&#x20;

И обратите внимание, что там реализация кнопки "Назад", которая работает адекватно.

{% code title="bot.py" %}

```python
    if message.text == "Назад":
        db_users.set_state(user_id, config.S_GET_CAT)
        get_categories(message)
```

{% endcode %}

То есть если из какой-либо из функций выбора товара пользователь захотел вернуться назад к категориям, то обработка происходит именно тут. Я на 2 строке перезаписываю состояние пользователя в БД и на 3 строке вызываю функцию категорий.

Больше в принципе пояснять нечего. Вроде все объяснил. Но чтобы вы почувствовали мое беспокойство, вспомните еще раз мои слова о том, что каждый компонент это бота, будь то список категорий, конкретная категория, список товаров или конкретный товар - для всего своя функция. А теперь прикиньте сколько имеется товара. И для каждого своя функция! При этом они однотипны. Надо сделать какую-то абстракцию, чтобы не штамповать 100500 функций.

### Щепотка абстракции

Немного передохнув, я вновь сел созидать. Но в процессе отдыха я понял, что совершаю еще несколько ошибок.&#x20;

Во-первых, я слишком сильно заострил внимание на содержании бота. Какой смысл было добавлять столько товара в каждую категорию в файле **gh\_menu**? Ведь моя задача на данном этапе заключается, грубо говоря, в **проверке гипотезы** - **нужен ли этот бот людям**, в частности кофейням. Исходя из этого я немного подредактировал файл `gh_menu.py`, просто оставив везде по два товара

{% code title="gh\_menu.py" %}

```python
gh_menu = {
    'Особые напитки': {
        'Латте Лаванда Шалфей': {
            '300 мл - 139 руб': '139',
            '400 мл - 169 руб': '169'
        },
        'Раф Лимонный Пай': {
            '300 мл - 159 руб': '159',
            '400 мл - 179 руб': '179'
        }
    },
    'Кофе': {
        'Капучино': {
            '300 мл - 119 руб': '119',
            '400 мл - 149 руб': '149'
        },
        'Латте Макиато': {
            '300 мл - 119 руб': '119',
            '400 мл - 139 руб': '139'
        }
    },
    'Горячие напитки': {
        'Какао': {
            '300 мл - 119 руб': '119',
            '400 мл - 149 руб': '149'
        },
        'Чай': {
            'Английский завтрак': {
                '300 мл - 99 руб': '99',
                '400 мл - 109 руб': '109'
            },
            'Эрл грей': {
                '300 мл - 99 руб': '99',
                '400 мл - 109 руб': '109'
            }
        }
    }
}
```

{% endcode %}

Во-вторых, я слишком париался по поводу "неидеальности" кода. Да, писать только на "говне и костылях" не лучший вариант. Но и программирование через призму перфекционизма, опять же, на данном этапе лишнее. Пусть где-то много кода. Его наверное всегда будет много. И всегда можно сделать эффективнее и короче. Но опять же - на данном этапе надо делать как знаешь. Вот когда я покажу прототип кофейням, кто-то заинтересуется, то и стоит об это задуматься.

И все-таки мне удалось поработать с объектами, которые уменьшили как количество кода, так и его дублирование. Сперва напомню, что в файле `bot.py` функции получения товаров категорий типа `get_special_drinks()` , `get_coffee()`, `get_hot_drinks()` практически идентичны. И пусть они по объему маленькие, но что если бы каждая функция была по 100 строк? Именно это дело я и подкорректировал.&#x20;

Отличие всех трех функций в том, что мы получаем получаем разные данные из словаря gh\_menu типа `gh_menu['сюда передаем категорию']`, и в разных состояниях, которые мы передаем в конце функций `db_users.set_state(user_id, config.<сюда состояние>`.

Поэтому я перешел в файл `config.py` и создал после всего имеющегося класс **Goods**.&#x20;

{% code title="config.py" %}

```python
# выше код...

class Goods:
    def __init__(self, bot, message, state):
        self.bot = bot
        self.message = message
        self.state = state

    def get_goods_list(self):
        """
        This function allows to get a list of special drinks
        """
        user_id = self.message.from_user.id
        mass = list(gh_menu[self.message.text])
        markup = create_menu(mass)
        self.bot.send_message(user_id, 'Выберите напиток', reply_markup=markup)
        db_users.set_state(user_id, self.state)
```

{% endcode %}

Класс имеет конструктор `def __init__()`, куда передаются следующие параметры:&#x20;

* **self** - это ссылка на экземпляр этого класса. В python он является обязательным, если вы работаете с классом. Если не в курсе про self, то погуглите, это не сложная, но важная тема.
* **bot** - это объект нашего бота, который мы создаем в самом начале файла **bot.py** (`bot = telebot.TeleBot(config.TOKEN)` )
* **message** - это объект библиотеки **pyTelegramBotAPI**, он нужен для получения данных о пользователе, отправленным им текстом и еще много всего.
* **state** - состояние пользователя, которое мы хотим ему присвоить

Также класс имеет функцию `get_goods_list()`, в которую я и вынес весь функционал. Теперь я редактирую функции в `bot.py`

{% code title="bot.py" %}

```python
# выше код...

@bot.message_handler(func=lambda message: db_users.get_current_state(message.from_user.id) == config.S_SPECIAL_DRINKS)
def get_special_drinks(message):
    """
    This function allows to get a list of special drinks
    """
    special_drinks = Goods(bot, message, config.S_CHOOSE_GOOD)
    special_drinks.get_goods_list()


@bot.message_handler(func=lambda message: db_users.get_current_state(message.from_user.id) == config.S_COFFEE)
def get_coffee(message):
    """
    This function allows to get a list of coffee
    """
    coffee = Goods(bot, message, config.S_CHOOSE_GOOD)
    coffee.get_goods_list()


@bot.message_handler(func=lambda message: db_users.get_current_state(message.from_user.id) == config.S_HOT_DRINKS)
def get_hot_drinks(message):
    """
    This function allows to get a list of hot drinks
    """
    hot_drinks = Goods(bot, message, config.S_CHOOSE_GOOD)
    hot_drinks.get_goods_list()

    # ниже код...
```

{% endcode %}

�И хоть количество функций не изменилось, и мне по-прежнему нужно создавать для каждой категории свою функцию, но их объем уменьшился, код стал читабельнее.

А что касается логики, то в каждой функции я создаю сначала экземпляр класса `Goods` (главное не забыть его импортировать `from config import Goods` ), передаю в его конструктор необходимые параметры, а после вызываю метод этого класса.

Аналогичную логику я применил и для работы с каждым отдельным товаром. Так как класс Goods создан для работы с товаром, то в нем же создаю еще один метод `get_current_good()`, который позволит получать характеристики отдельного товара.

{% code title="config.py" %}

```python
# выше код...

class Goods:
    def __init__(self, bot, message, state):
        self.bot = bot
        self.message = message
        self.state = state

    def get_goods_list(self):
        """
        This function allows to get a list of special drinks
        """
        user_id = self.message.from_user.id
        mass = list(gh_menu[self.message.text])
        markup = create_menu(mass)
        self.bot.send_message(user_id, 'Выберите напиток', reply_markup=markup)
        db_users.set_state(user_id, self.state)

    def get_current_good(self, cat):
        """
        This function allows to get a current good
        """
        user_id = self.message.from_user.id
        mass = list(gh_menu[cat][self.message.text])
        markup = create_menu(mass)
        self.bot.send_message(user_id, 'Выберите размер', reply_markup=markup)
        db_users.set_state(user_id, self.state)
```

{% endcode %}

Этот метод практически копирует предыдущий, за исключением того, что требует еще один параметр `cat` - это название категории типа "**Особые напитки**", "**Кофе**" и тп. Ведь я работаю со словарем **gh\_menu**, соответсвенно сначала по ключам категорий получаю товары, а потом эти же товары выступают в качестве ключей, по которым я получаю цены и прочие характеристики.

### Содержание каждого из файлов на данных момент

Я посчитал нужным подвести промежуточный итог, показав содержимое каждого файла, на случай, если кто-то где-то потерялся, либо я случайно о чем-то умолчал:)

Сейчас бот имеет 4 файла

* `bot.py`
* `config.py`
* `gh_menu.py`
* `db_users.py`

{% code title="bot.py" %}

```python
import telebot
from telebot import types
from gh_menu import gh_menu
import db_users
from config import create_menu, Goods
import config

bot = telebot.TeleBot(config.TOKEN)

text_messages = {
    'start':
        u'Приветствую тебя, {name}!\n'
        u'Я помогу тебе сделать онлайн заказ (это быстро и без очереди).\n\n'
        u'1. Выбери интересующий напиток/сэндвич/десерт (Ты можешь выбрать несколько)\n'
        u'2. Выбери время, когда захочешь забрать заказ\n'
        u'3. Оплати заказ (это безопасно)\n'
        u'4. Обязательно забери заказ вовремя',
    
    'help':
        u'Пока что я не знаю, чем тебе помочь, поэтому просто выпей кофе!'
}


@bot.message_handler(commands=['start'])
def send_welcome(message):
    db_users.check_and_add_user(message)

    markup = types.ReplyKeyboardMarkup(resize_keyboard=True)
    markup.row('Сделать заказ')
    bot.send_message(message.from_user.id, text_messages['start'].format(name=message.from_user.first_name), reply_markup=markup)
    db_users.set_state(message.from_user.id, config.S_GET_CAT)


@bot.message_handler(commands=['help'])
def send_help(message):
    markup = types.ReplyKeyboardMarkup(resize_keyboard=True)
    markup.row('Назад')
    msg = bot.send_message(message.from_user.id, text_messages['help'], reply_markup=markup)
    bot.register_next_step_handler(msg, send_welcome)


@bot.message_handler(func=lambda message: db_users.get_current_state(message.from_user.id) == config.S_GET_CAT)
def get_categories(message):
    """
    This function allows to get a list of categories
    """
    user_id = message.from_user.id
    mass = list(gh_menu.keys())
    markup = create_menu(mass, back=False)
    bot.send_message(user_id, 'Что вас интересует?', reply_markup=markup)
    db_users.set_state(user_id, config.S_CHOOSE_CAT)



# !!!  ЭТО ПРОМЕЖУТОЧНАЯ ФУНКЦИЯ  !!!
@bot.message_handler(func=lambda message: db_users.get_current_state(message.from_user.id) == config.S_CHOOSE_CAT)
def choose_categories(message):
    """
    This function allows to choose a category
    """
    user_id = message.from_user.id

    if message.text == 'Особые напитки':
        db_users.set_state(user_id, config.S_SPECIAL_DRINKS)
        get_special_drinks(message)
    elif message.text == 'Кофе':
        db_users.set_state(user_id, config.S_COFFEE)
        get_coffee(message)
    elif message.text == 'Горячие напитки':
        db_users.set_state(user_id, config.S_HOT_DRINKS)
        get_hot_drinks(message)



@bot.message_handler(func=lambda message: db_users.get_current_state(message.from_user.id) == config.S_SPECIAL_DRINKS)
def get_special_drinks(message):
    """
    This function allows to get a list of special drinks
    """
    special_drinks = Goods(bot, message, config.S_CHOOSE_GOOD)
    special_drinks.get_goods_list()


@bot.message_handler(func=lambda message: db_users.get_current_state(message.from_user.id) == config.S_COFFEE)
def get_coffee(message):
    """
    This function allows to get a list of coffee
    """
    coffee = Goods(bot, message, config.S_CHOOSE_GOOD)
    coffee.get_goods_list()


@bot.message_handler(func=lambda message: db_users.get_current_state(message.from_user.id) == config.S_HOT_DRINKS)
def get_hot_drinks(message):
    """
    This function allows to get a list of hot drinks
    """
    hot_drinks = Goods(bot, message, config.S_CHOOSE_GOOD)
    hot_drinks.get_goods_list()


# ЭТО ПРОМЕЖУТОЧНАЯ ФУНКЦИЯ
@bot.message_handler(func=lambda message: db_users.get_current_state(message.from_user.id) == config.S_CHOOSE_GOOD)
def choose_good(message):
    """
    This function allows to choose a goods
    """
    user_id = message.from_user.id

    if message.text == "Назад":
        db_users.set_state(user_id, config.S_GET_CAT)
        get_categories(message)
    if message.text == 'Латте Лаванда Шалфей':
        db_users.set_state(user_id, config.S_LATTE_LAVANDA_SHALFEI)
        get_latte_lavanda_shalfei(message)
    elif message.text == 'Раф Лимонный Пай':
        db_users.set_state(user_id, config.S_RAF_LEMON_PIE)
        get_raf_lemon_pie(message)
    elif message.text == 'Капучино':
        db_users.set_state(user_id, config.S_KAPUCHINO)
        get_kapuchino(message)
    elif message.text == 'Латте Макиато':
        db_users.set_state(user_id, config.S_LATTE_MAKIATO)
        get_latte_makiato(message)
    elif message.text == 'Какао':
        db_users.set_state(user_id, config.S_KAKAO)
        get_kakao(message)
    elif message.text == 'Чай':
        db_users.set_state(user_id, config.S_TEA)
        get_tea(message)



@bot.message_handler(func=lambda message: db_users.get_current_state(message.from_user.id) == config.S_LATTE_LAVANDA_SHALFEI)
def get_latte_lavanda_shalfei(message):
    """
    This function allows to get a parameters of latte_lavanda_shalfei
    """
    latte_lavanda_shalfei = Goods(bot, message, config.S_LATTE_LAVANDA_SHALFEI)
    latte_lavanda_shalfei.get_current_good(config.S_SPECIAL_DRINKS)


@bot.message_handler(func=lambda message: db_users.get_current_state(message.from_user.id) == config.S_RAF_LEMON_PIE)
def get_raf_lemon_pie(message):
    """
    This function allows to get a parameters of raf_lemon_pie
    """
    raf_lemon_pie = Goods(bot, message, config.S_RAF_LEMON_PIE)
    raf_lemon_pie.get_current_good(config.S_SPECIAL_DRINKS)


@bot.message_handler(func=lambda message: db_users.get_current_state(message.from_user.id) == config.S_KAPUCHINO)
def get_kapuchino(message):
    """
    This function allows to get a parameters of kapuchino
    """
    kapuchino = Goods(bot, message, config.S_KAPUCHINO)
    kapuchino.get_current_good(config.S_COFFEE)


@bot.message_handler(func=lambda message: db_users.get_current_state(message.from_user.id) == config.S_LATTE_MAKIATO)
def get_latte_makiato(message):
    """
    This function allows to get a parameters of latte_makiato
    """
    latte_makiato = Goods(bot, message, config.S_LATTE_MAKIATO)
    latte_makiato.get_current_good(config.S_COFFEE)


@bot.message_handler(func=lambda message: db_users.get_current_state(message.from_user.id) == config.S_KAKAO)
def get_kakao(message):
    """
    This function allows to get a parameters of kakao
    """
    kakao = Goods(bot, message, config.S_KAKAO)
    kakao.get_current_good(config.S_HOT_DRINKS)


@bot.message_handler(func=lambda message: db_users.get_current_state(message.from_user.id) == config.S_TEA)
def get_tea(message):
    """
    This function allows to get a parameters of tea
    """
    tea = Goods(bot, message, config.S_TEA)
    tea.get_current_good(config.S_HOT_DRINKS)



bot.polling(none_stop=True)
```

{% endcode %}

�

{% code title="config.py" %}

```python
from telebot import types

import db_users
from gh_menu import gh_menu

TOKEN = 'Ваш токен'


# КОНСТАНТЫ СОСТОЯНИЙ
S_GET_CAT = 'Список категорий'
S_CHOOSE_CAT = 'Выбор категории'

# КОНСТАНТЫ КАТЕГОРИЙ
S_SPECIAL_DRINKS = 'Особые напитки'
S_COFFEE = 'Кофе'
S_HOT_DRINKS = 'Горячие напитки'

# КОНСТАНТЫ ТОВАРОВ
S_CHOOSE_GOOD = 'Выбор товара'

S_LATTE_LAVANDA_SHALFEI = 'Латте Лаванда Шалфей'
S_RAF_LEMON_PIE = 'Раф Лимонный Пай'


S_KAPUCHINO = 'Капучино'
S_LATTE_MAKIATO = 'Латте Макиато'


S_KAKAO = 'Какао'
S_TEA = 'Чай'





#ОСОБЫЕ НАПИТКИ
list_of_latte = ['Латте Лаванда Шалфей']




def create_menu(mass, back=True):
    """
    This function allows to creat menu of buttons.
    mass - the list of string
    back - back button, if true, add a button back. Default back=True
    """
    markup = types.ReplyKeyboardMarkup(resize_keyboard=True)

    if len(mass) == 1:
        markup.row(mass[0])
    else:
        while len(mass) > 0:
            try:
                cut = mass[:2]
                markup.row(cut[0], cut[1])
                del mass[:2]
                
                if len(mass) == 1:
                    markup.row(mass[0])
                    break
            except:
                print('WTF')
    

    if back == True:
        markup.row('Назад')
    
    return markup


class Goods:
    def __init__(self, bot, message, state):
        self.bot = bot
        self.message = message
        self.state = state

    def get_goods_list(self):
        """
        This function allows to get a list of special drinks
        """
        user_id = self.message.from_user.id
        mass = list(gh_menu[self.message.text])
        markup = create_menu(mass)
        self.bot.send_message(user_id, 'Выберите напиток', reply_markup=markup)
        db_users.set_state(user_id, self.state)

    def get_current_good(self, cat):
        """
        This function allows to get a current good
        """
        user_id = self.message.from_user.id
        mass = list(gh_menu[cat][self.message.text])
        markup = create_menu(mass)
        self.bot.send_message(user_id, 'Выберите размер', reply_markup=markup)
        db_users.set_state(user_id, self.state)
```

{% endcode %}

�

{% code title="gh\_menu.py" %}

```python
gh_menu = {
    'Особые напитки': {
        'Латте Лаванда Шалфей': {
            '300 мл - 139 руб': '139',
            '400 мл - 169 руб': '169'
        },
        'Раф Лимонный Пай': {
            '300 мл - 159 руб': '159',
            '400 мл - 179 руб': '179'
        }
    },
    'Кофе': {
        'Капучино': {
            '300 мл - 119 руб': '119',
            '400 мл - 149 руб': '149'
        },
        'Латте Макиато': {
            '300 мл - 119 руб': '119',
            '400 мл - 139 руб': '139'
        }
    },
    'Горячие напитки': {
        'Какао': {
            '300 мл - 119 руб': '119',
            '400 мл - 149 руб': '149'
        },
        'Чай': {
            'Английский завтрак': {
                '300 мл - 99 руб': '99',
                '400 мл - 109 руб': '109'
            },
            'Эрл грей': {
                '300 мл - 99 руб': '99',
                '400 мл - 109 руб': '109'
            }
        }
    }
}
```

{% endcode %}

�

{% code title="db\_users.py" %}

```python
from pymongo import MongoClient
from datetime import datetime

client = MongoClient('localhost', 27017)

db = client['gh_coffee_db']


def check_and_add_user(message):
    if db.users.find_one({'user_id': message.from_user.id}) == None:
        new_user = {
            'first_name': message.from_user.first_name,
            'last_name': message.from_user.last_name,
            'user_id': message.from_user.id,
            'date': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
            'state': 'старт'
        }
        db.users.insert_one(new_user)
    return

def get_current_state(user_id):
    user = db.users.find_one({'user_id':user_id})
    return user['state']


def set_state(user_id, state_value):
    db.users.update_one({'user_id': user_id}, {"$set": {'state': state_value}})
```

{% endcode %}

�(**СТАТЬЯ ЕЖЕДНЕВНО ДОПИСЫВАЕТСЯ И РЕДАКТИРУЕТСЯ**)
